v0.3: phone check (iPhone, Android) as the Space front page, catalog 0.3 (74 checks), shared vulnerability rules, real-CVE training data
Browse files- .gitignore +2 -0
- README.md +18 -3
- about.html +161 -0
- app/renderer/auditor-core.js +15 -1
- app/renderer/model-core.js +47 -0
- app/renderer/model.js +4 -45
- app/renderer/report.js +15 -6
- app/renderer/vuln-core.js +228 -0
- app/test/prompt-parity.mjs +2 -2
- app/vulndb.cjs +8 -224
- baseline.conf +6 -0
- catalog/checks.json +1823 -83
- catalog/mobile_questions.json +170 -0
- collectors/linux/audit.sh +1 -1
- collectors/macos/audit.sh +1 -1
- collectors/windows/audit.ps1 +1 -1
- docs/CONTROLS.md +86 -79
- index.html +78 -142
- library/ai_tools.json +170 -18
- library/check_guidance.json +885 -212
- mobile/app.js +352 -0
- mobile/build-evidence.mjs +33 -0
- mobile/detect.js +76 -0
- mobile/mobile-core.js +216 -0
- mobile/styles.css +66 -0
- mobile/test/e2e.cjs +58 -0
- mobile/vuln-web.js +35 -0
- schema/evidence.schema.json +7 -4
- synthetic/generate_evidence.py +280 -39
- synthetic/samples/synthetic-android-42000126.json +346 -0
- synthetic/samples/synthetic-ios-42000126.json +331 -0
- synthetic/samples/synthetic-linux-42000126.json +37 -37
- synthetic/samples/synthetic-macos-42000126.json +37 -35
- synthetic/samples/synthetic-windows-42000126.json +33 -30
- synthetic/vuln-helper.mjs +17 -0
- tools/build_controls_doc.py +37 -6
- tools/validate_evidence.py +10 -4
- training/build_dataset.py +24 -2
- vulndb/build_bundle.py +20 -6
- vulndb/products.json +3 -1
- vulndb/sign-bundle.mjs +5 -0
.gitignore
CHANGED
|
@@ -15,3 +15,5 @@ synthetic/out-v0.2/
|
|
| 15 |
training/out-v0.2/
|
| 16 |
vulndb/cache/
|
| 17 |
vulndb/out/
|
|
|
|
|
|
|
|
|
| 15 |
training/out-v0.2/
|
| 16 |
vulndb/cache/
|
| 17 |
vulndb/out/
|
| 18 |
+
synthetic/out-v0.3/
|
| 19 |
+
training/out-v0.3/
|
README.md
CHANGED
|
@@ -151,6 +151,20 @@ npm start # run the app
|
|
| 151 |
|
| 152 |
Offline fine-tuning on CPU: `training/train_lora.py` (LoRA on Qwen2.5-0.5B-Instruct; see `--benchmark` and `--resume`).
|
| 153 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
## Vulnerability matching (offline)
|
| 155 |
|
| 156 |
`vuln.known_vulnerabilities` is decided by exact version comparison, never by the model:
|
|
@@ -165,10 +179,11 @@ Devseis builds the bundle with `python3 vulndb/build_bundle.py` (needs internet;
|
|
| 165 |
|
| 166 |
| Part | Version | Notes |
|
| 167 |
|---|---|---|
|
| 168 |
-
| Catalog, library, synthetic data, training data | **0.
|
| 169 |
-
| Collectors (Windows, Linux, macOS) | **0.2** | macOS tested on a real Mac; Linux tested on Ubuntu 24.04 and Fedora 42 containers; Windows parse-checked only (needs a real Windows test). Known vulnerabilities: inventory only, matching comes with the offline vulnerability bundle |
|
| 170 |
| Vulnerability matcher and bundle | 0.1 | Tested: this Mac (Chrome 154 → 58 critical/high, fixed in 155), Ubuntu 24.04 (OpenSSL), Rocky 9 (25 of 25 packages agree with dnf's own security list), Fedora reported as not covered |
|
| 171 |
-
|
|
|
|
|
| 172 |
|
| 173 |
## Test status (collector version 0.1.0)
|
| 174 |
|
|
|
|
| 151 |
|
| 152 |
Offline fine-tuning on CPU: `training/train_lora.py` (LoRA on Qwen2.5-0.5B-Instruct; see `--benchmark` and `--resume`).
|
| 153 |
|
| 154 |
+
## Phone check (iPhone, iPad, Android)
|
| 155 |
+
|
| 156 |
+
The Space's front page (`index.html`) is a phone self-check that runs entirely in the browser:
|
| 157 |
+
|
| 158 |
+
1. **Detects** what a browser can see: OS and version (Safari 26+ hides the iOS version in its user agent, so Safari's own version is used; Android uses client hints), device model (Android), browser, whether a device unlock method is set up (WebAuthn), and WebGPU support.
|
| 159 |
+
2. **Asks** the rest, one question at a time with *where to look* in Settings (`catalog/mobile_questions.json`). Every result is labelled `detected` or `self-reported` in the evidence and the report.
|
| 160 |
+
3. **Matches** the iOS version against the signed phone subset of the vulnerability bundle (55 KB, signature checked with WebCrypto).
|
| 161 |
+
4. **Writes** the findings with the same auditor model in WebLLM (about 350 MB download, roughly 1 GB of GPU memory, so newer phones) or with the built-in writer, and builds the same report as the desktop app.
|
| 162 |
+
5. **Saves or shares** the report as a PDF (share sheet or download). Phones do not allow a silent auto-save.
|
| 163 |
+
|
| 164 |
+
`mobile/mobile-core.js` turns detections and answers into evidence; the synthetic generator calls the same code through `mobile/build-evidence.mjs`, so training data has exactly the page's wording. Organisation answers can be passed in the link: `index.html#org=<base64 JSON {"ORG_ROPA": "yes", ...}>`.
|
| 165 |
+
|
| 166 |
+
Test without a phone: serve the repo (`python3 -m http.server 8765`) and run `app/node_modules/.bin/electron mobile/test/e2e.cjs ios out.json` (or `android`); it answers every question in a phone-sized window with a phone user agent and builds the PDF; then check the evidence with `python3 tools/validate_evidence.py out.json`.
|
| 167 |
+
|
| 168 |
## Vulnerability matching (offline)
|
| 169 |
|
| 170 |
`vuln.known_vulnerabilities` is decided by exact version comparison, never by the model:
|
|
|
|
| 179 |
|
| 180 |
| Part | Version | Notes |
|
| 181 |
|---|---|---|
|
| 182 |
+
| Catalog, library, synthetic data, training data | **0.3** | 74 checks: Windows 64, Linux 56, macOS 56, iPhone 29, Android 30. 0.3 adds phones, real CVEs in training data and wording identical to the collectors and the phone page; 39,500 grounded examples |
|
| 183 |
+
| Collectors (Windows, Linux, macOS) | **0.2** (catalog 0.3) | macOS tested on a real Mac; Linux tested on Ubuntu 24.04 and Fedora 42 containers; Windows parse-checked only (needs a real Windows test). Known vulnerabilities: inventory only, matching comes with the offline vulnerability bundle |
|
| 184 |
| Vulnerability matcher and bundle | 0.1 | Tested: this Mac (Chrome 154 → 58 critical/high, fixed in 155), Ubuntu 24.04 (OpenSSL), Rocky 9 (25 of 25 packages agree with dnf's own security list), Fedora reported as not covered |
|
| 185 |
+
| Phone check (web) | **0.3** | Tested in a phone-sized window as iPhone (Safari 26) and Android (Chrome 141); needs testing on real phones |
|
| 186 |
+
| Fine-tuned model | training on 0.1 data | First run in progress on CPU; next run on 0.3 data |
|
| 187 |
|
| 188 |
## Test status (collector version 0.1.0)
|
| 189 |
|
about.html
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>Devseis Endpoint Auditor</title>
|
| 7 |
+
<meta name="description" content="Read-only endpoint audit for ISO 27001, GDPR and the EU AI Act. Stage 1, step 1: collectors, evidence format and synthetic data.">
|
| 8 |
+
<style>
|
| 9 |
+
:root {
|
| 10 |
+
--bg: #f7f8fb; --card: #ffffff; --text: #16181d; --muted: #5b6270; --line: #e3e6ec;
|
| 11 |
+
--red: #ED2939; --blue: #00A1DE; --ok: #1a7f37; --bad: #c4242f; --na: #6e7781; --pending: #9a6700; --err: #8250df;
|
| 12 |
+
--code-bg: #0f1218; --code-text: #e6edf3;
|
| 13 |
+
}
|
| 14 |
+
@media (prefers-color-scheme: dark) {
|
| 15 |
+
:root:not([data-theme="light"]) {
|
| 16 |
+
--bg: #0d1117; --card: #161b22; --text: #e6edf3; --muted: #9da7b3; --line: #2b313a;
|
| 17 |
+
--ok: #3fb950; --bad: #ff6b6b; --na: #8b949e; --pending: #d29922; --err: #b392f0; --code-bg: #0a0d12;
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
* { box-sizing: border-box; }
|
| 21 |
+
body { margin: 0; background: var(--bg); color: var(--text); font: 16px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
|
| 22 |
+
main { max-width: 1040px; margin: 0 auto; padding: 32px 16px 64px; }
|
| 23 |
+
.tag { display: inline-block; font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--red); border: 1px solid var(--red); border-radius: 999px; padding: 2px 10px; }
|
| 24 |
+
h1 { font-size: clamp(28px, 5vw, 40px); line-height: 1.15; margin: 14px 0 8px; }
|
| 25 |
+
h2 { font-size: 20px; margin: 36px 0 12px; }
|
| 26 |
+
p.lead { color: var(--muted); font-size: 18px; max-width: 760px; margin: 0; }
|
| 27 |
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; margin-top: 20px; }
|
| 28 |
+
.card { background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 16px; }
|
| 29 |
+
.card b { display: block; font-size: 15px; margin-bottom: 4px; }
|
| 30 |
+
.card span { color: var(--muted); font-size: 14px; }
|
| 31 |
+
.tabs { display: flex; gap: 8px; flex-wrap: wrap; margin: 12px 0; }
|
| 32 |
+
.tabs button { font: inherit; font-size: 14px; padding: 6px 14px; border-radius: 999px; border: 1px solid var(--line); background: var(--card); color: var(--text); cursor: pointer; }
|
| 33 |
+
.tabs button[aria-selected="true"] { background: var(--blue); border-color: var(--blue); color: #fff; }
|
| 34 |
+
.counts { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
|
| 35 |
+
.pill { font-size: 13px; font-weight: 600; padding: 3px 10px; border-radius: 999px; background: var(--card); border: 1px solid var(--line); }
|
| 36 |
+
.s-Compliant { color: var(--ok); } .s-Non-Compliant { color: var(--bad); } .s-NotApplicable { color: var(--na); }
|
| 37 |
+
.s-Pending { color: var(--pending); } .s-Error { color: var(--err); }
|
| 38 |
+
.split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr); gap: 12px; }
|
| 39 |
+
@media (max-width: 760px) { .split { grid-template-columns: 1fr; } }
|
| 40 |
+
pre { margin: 0; background: var(--code-bg); color: var(--code-text); border-radius: 12px; padding: 14px; font: 12.5px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; overflow: auto; max-height: 560px; }
|
| 41 |
+
table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
|
| 42 |
+
.table-wrap { background: var(--card); border: 1px solid var(--line); border-radius: 12px; overflow: auto; max-height: 560px; }
|
| 43 |
+
th, td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
| 44 |
+
th { position: sticky; top: 0; background: var(--card); font-size: 12px; text-transform: uppercase; letter-spacing: .03em; color: var(--muted); }
|
| 45 |
+
td.ref { color: var(--muted); font-size: 12.5px; }
|
| 46 |
+
code { font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: 1px 6px; }
|
| 47 |
+
.note { border-left: 4px solid var(--red); background: var(--card); padding: 12px 16px; border-radius: 0 12px 12px 0; color: var(--muted); }
|
| 48 |
+
ol li { margin: 4px 0; }
|
| 49 |
+
a { color: var(--blue); }
|
| 50 |
+
footer { margin-top: 48px; color: var(--muted); font-size: 14px; }
|
| 51 |
+
</style>
|
| 52 |
+
</head>
|
| 53 |
+
<body>
|
| 54 |
+
<main>
|
| 55 |
+
<span class="tag">Open source · stage 1</span>
|
| 56 |
+
<h1>Devseis Endpoint Auditor</h1>
|
| 57 |
+
<p class="lead">A read-only audit of Windows, Linux, macOS, iPhone and Android devices against a security baseline, GDPR technical controls, EU AI Act controls and organisational controls, written up by a small AI auditor model that runs on the device. <a href="index.html">Check your phone now</a>.</p>
|
| 58 |
+
|
| 59 |
+
<div class="grid">
|
| 60 |
+
<div class="card"><b>Read-only collectors</b><span>PowerShell, Linux shell and macOS shell scripts. They read settings and never change the system.</span></div>
|
| 61 |
+
<div class="card"><b>One evidence format</b><span>Same JSON for every OS, joined to a catalog with ISO 27001, GDPR and AI Act references.</span></div>
|
| 62 |
+
<div class="card"><b>Synthetic data</b><span>Realistic evidence with the same rules, so the auditor model can be built before real data exists.</span></div>
|
| 63 |
+
<div class="card"><b>Runs locally</b><span>Evidence stays on the device. Nothing is sent anywhere.</span></div>
|
| 64 |
+
<div class="card"><b>Offline vulnerability matching</b><span>Installed software is matched against a signed OSV/NVD/CISA KEV bundle by exact version rules.</span></div>
|
| 65 |
+
<div class="card"><b>Phones too</b><span>A browser check for iPhone and Android: what the browser can detect plus guided questions, each result labelled detected or self-reported.</span></div>
|
| 66 |
+
</div>
|
| 67 |
+
|
| 68 |
+
<h2>Sample audit (synthetic)</h2>
|
| 69 |
+
<div class="tabs" role="tablist" id="tabs"></div>
|
| 70 |
+
<div class="counts" id="counts"></div>
|
| 71 |
+
<div class="split">
|
| 72 |
+
<pre id="report" aria-label="Text report">Loading…</pre>
|
| 73 |
+
<div class="table-wrap">
|
| 74 |
+
<table>
|
| 75 |
+
<thead><tr><th>Check</th><th>Status</th><th>Found</th><th>References</th></tr></thead>
|
| 76 |
+
<tbody id="rows"></tbody>
|
| 77 |
+
</table>
|
| 78 |
+
</div>
|
| 79 |
+
</div>
|
| 80 |
+
|
| 81 |
+
<h2>Roadmap</h2>
|
| 82 |
+
<ol>
|
| 83 |
+
<li><b>Collectors, evidence format, catalog (74 checks), synthetic data</b> — done</li>
|
| 84 |
+
<li><b>Control library and fix guidance</b> in Devseis's own wording — done; expert review welcome</li>
|
| 85 |
+
<li><b>Training data</b> v0.3: 39,500 grounded examples for Windows, Linux, macOS, iPhone and Android — done</li>
|
| 86 |
+
<li><b>Offline vulnerability matching</b> (OSV, NVD, CISA KEV, EPSS), signed bundle — done</li>
|
| 87 |
+
<li><b>Desktop app</b> (Electron + WebLLM) and <b>phone check</b> (this Space) — done, with the base model</li>
|
| 88 |
+
<li>Fine-tune Qwen2.5-0.5B-Instruct on this Mac (CPU, LoRA) — in progress</li>
|
| 89 |
+
<li>Evaluate on held-out data, publish the scores and the model</li>
|
| 90 |
+
<li>Installers per OS; tests on real Windows and Linux machines</li>
|
| 91 |
+
</ol>
|
| 92 |
+
|
| 93 |
+
<p class="note">Not a certification. ISO 27001 certification needs an accredited auditor, and much of GDPR and AI Act compliance is organisational. The tool collects evidence and flags gaps. See the <a href="README.md">README</a> for how to run the collectors.</p>
|
| 94 |
+
|
| 95 |
+
<footer>Open source by Devseis: code Apache-2.0, data CC BY 4.0 · <a href="https://huggingface.co/datasets/Devseis/endpoint-auditor-synthetic">training data</a> · <a href="https://huggingface.co/datasets/Devseis/endpoint-auditor-vulndb">vulnerability data</a> · <a href="CITATION.cff">cite</a></footer>
|
| 96 |
+
</main>
|
| 97 |
+
<script>
|
| 98 |
+
const SAMPLES = Object.fromEntries(['windows', 'linux', 'macos', 'ios', 'android'].map(os => [os, `synthetic/samples/synthetic-${os}-42000126.json`]));
|
| 99 |
+
const LABELS = { windows: 'Windows', linux: 'Linux', macos: 'macOS', ios: 'iPhone', android: 'Android' };
|
| 100 |
+
const LAYOUT = {
|
| 101 |
+
windows: { title: 'WINDOWS ENDPOINT SECURITY BASELINE - AUDIT', results: 'COMPLIANCE RESULTS', end: 'END OF READ-ONLY AUDIT', blank: true, extra: ['Build', 'build'] },
|
| 102 |
+
linux: { title: 'LINUX ENDPOINT SECURITY BASELINE - AUDIT', results: 'LINUX ENDPOINT SECURITY BASELINE - RESULTS', end: 'END OF LINUX ENDPOINT SECURITY BASELINE AUDIT', blank: false, extra: ['Kernel', 'kernel'] },
|
| 103 |
+
macos: { title: 'macOS ENDPOINT SECURITY BASELINE - AUDIT', results: 'COMPLIANCE RESULTS', end: 'END OF AUDIT', blank: false, extra: ['Build', 'build'] },
|
| 104 |
+
ios: { title: 'iPHONE SECURITY CHECK (BROWSER)', results: 'COMPLIANCE RESULTS', end: 'END OF PHONE CHECK', blank: false, extra: ['Browser', 'browser'] },
|
| 105 |
+
android: { title: 'ANDROID SECURITY CHECK (BROWSER)', results: 'COMPLIANCE RESULTS', end: 'END OF PHONE CHECK', blank: false, extra: ['Browser', 'browser'] },
|
| 106 |
+
};
|
| 107 |
+
const SECTIONS = [['baseline', null], ['gdpr', 'GDPR TECHNICAL CONTROLS'], ['aiact', 'EU AI ACT CONTROLS'], ['org', 'ORGANISATIONAL CONTROLS']];
|
| 108 |
+
let catalog = null;
|
| 109 |
+
|
| 110 |
+
function render(rec) {
|
| 111 |
+
const L = LAYOUT[rec.host.os_family], line = '='.repeat(60), pad = s => (s + ' '.repeat(16)).slice(0, 16);
|
| 112 |
+
const out = [line, ' ' + L.title, line,
|
| 113 |
+
pad('Hostname') + ': ' + rec.host.hostname, pad('Operating System') + ': ' + rec.host.os_name,
|
| 114 |
+
pad('OS Version') + ': ' + rec.host.os_version, pad(L.extra[0]) + ': ' + (rec.host[L.extra[1]] || ''),
|
| 115 |
+
pad('Serial Number') + ': ' + (rec.host.serial_number || ''), pad('Execution User') + ': ' + rec.audit.execution_user,
|
| 116 |
+
pad('Audit Date') + ': ' + rec.audit.started_at.replace('T', ' ').slice(0, 19), pad('Audit Mode') + ': ' + rec.audit.mode, line];
|
| 117 |
+
for (const [id, title] of SECTIONS) {
|
| 118 |
+
if (L.blank) out.push('');
|
| 119 |
+
out.push(line, ' ' + (title || L.results), line);
|
| 120 |
+
for (const r of rec.results) if (r.section === id) out.push(r.name, r.status + (r.evidence ? ` (${r.evidence})` : ''));
|
| 121 |
+
}
|
| 122 |
+
out.push(line, ' ' + L.end, line);
|
| 123 |
+
return out.join('\n');
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
function refs(id) {
|
| 127 |
+
const c = catalog && catalog.checks.find(x => x.id === id);
|
| 128 |
+
if (!c) return '';
|
| 129 |
+
return [...c.controls.iso27001.map(x => 'ISO ' + x), ...c.controls.gdpr.map(x => 'GDPR ' + x), ...c.controls.ai_act.map(x => 'AI Act ' + x)].join(' · ');
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
| 133 |
+
|
| 134 |
+
async function show(os) {
|
| 135 |
+
document.querySelectorAll('#tabs button').forEach(b => b.setAttribute('aria-selected', String(b.dataset.os === os)));
|
| 136 |
+
try {
|
| 137 |
+
const rec = await (await fetch(SAMPLES[os])).json();
|
| 138 |
+
document.getElementById('report').textContent = render(rec);
|
| 139 |
+
document.getElementById('counts').innerHTML = Object.entries(rec.summary)
|
| 140 |
+
.map(([k, v]) => `<span class="pill s-${k}">${v} ${k}</span>`).join('');
|
| 141 |
+
document.getElementById('rows').innerHTML = rec.results.map(r =>
|
| 142 |
+
`<tr><td>${esc(r.name)}</td><td class="s-${r.status}"><b>${r.status}</b></td><td>${esc(r.actual)}</td><td class="ref">${esc(refs(r.id))}</td></tr>`).join('');
|
| 143 |
+
} catch (e) {
|
| 144 |
+
document.getElementById('report').textContent = 'Could not load the sample: ' + e.message;
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
(async () => {
|
| 149 |
+
try { catalog = await (await fetch('catalog/checks.json')).json(); } catch (e) { catalog = null; }
|
| 150 |
+
const tabs = document.getElementById('tabs');
|
| 151 |
+
for (const os of Object.keys(SAMPLES)) {
|
| 152 |
+
const b = document.createElement('button');
|
| 153 |
+
b.textContent = LABELS[os]; b.dataset.os = os; b.setAttribute('role', 'tab');
|
| 154 |
+
b.addEventListener('click', () => show(os));
|
| 155 |
+
tabs.appendChild(b);
|
| 156 |
+
}
|
| 157 |
+
show('windows');
|
| 158 |
+
})();
|
| 159 |
+
</script>
|
| 160 |
+
</body>
|
| 161 |
+
</html>
|
app/renderer/auditor-core.js
CHANGED
|
@@ -58,6 +58,12 @@ const deviceLine = record => `${record.host.os_name} ${record.host.os_version} (
|
|
| 58 |
const sentence = text => { const t = text.trim(); return !t || /[.!?]$/.test(t) ? t : `${t}.`; };
|
| 59 |
|
| 60 |
// --- Task A: finding ----------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
export function findingPrompt(record, result, lib) {
|
| 62 |
const osFamily = record.host.os_family;
|
| 63 |
const baseline = record.baseline;
|
|
@@ -72,6 +78,7 @@ export function findingPrompt(record, result, lib) {
|
|
| 72 |
`REQUIRED: ${result.expected}`,
|
| 73 |
];
|
| 74 |
if (result.detail) lines.push(`DETAIL: ${result.detail}`);
|
|
|
|
| 75 |
lines.push('REFERENCES:');
|
| 76 |
for (const [label, title, summary] of lib.references(result.id)) lines.push(`- ${label} ${title}: ${summary}`);
|
| 77 |
lines.push('GUIDANCE:');
|
|
@@ -126,8 +133,10 @@ export function findingTemplate(record, result, lib) {
|
|
| 126 |
risk = { level: 'None', reason: 'Not applicable to this device.' };
|
| 127 |
recommendation = [];
|
| 128 |
}
|
|
|
|
| 129 |
const answer = { check_id: result.id, status, title, finding: finding.trim(), risk, recommendation, references };
|
| 130 |
if (verification) answer.verification = verification;
|
|
|
|
| 131 |
return answer;
|
| 132 |
}
|
| 133 |
|
|
@@ -142,7 +151,10 @@ export function summaryPrompt(record, lib) {
|
|
| 142 |
const { results, summary, host } = record;
|
| 143 |
const lines = ['TASK: summary', `DEVICE: ${deviceLine(record)}`, `HOSTNAME: ${host.hostname}`,
|
| 144 |
`AUDIT RUN AS ADMIN: ${record.audit.is_admin ? 'yes' : 'no'}`,
|
| 145 |
-
'COUNTS: ' + Object.entries(summary).map(([k, v]) => `${k}=${v}`).join(', ')
|
|
|
|
|
|
|
|
|
|
| 146 |
for (const r of results) lines.push(`- ${r.id} | ${r.name} | ${r.status} | ${r.actual}`);
|
| 147 |
lines.push('RISK LEVELS (from guidance):');
|
| 148 |
for (const r of results.filter(x => x.status === 'Non-Compliant')) lines.push(`- ${r.id}: ${lib.guide(r.id).risk.level}`);
|
|
@@ -172,6 +184,8 @@ export function summaryTemplate(record, lib) {
|
|
| 172 |
}
|
| 173 |
if (summary.Error) text += ` ${summary.Error} checks could not be read` + (record.audit.is_admin ? '.' : ' because the audit was not run as administrator.');
|
| 174 |
if (summary.Pending) text += ` ${summary.Pending} items are pending.`;
|
|
|
|
|
|
|
| 175 |
const nextSteps = [];
|
| 176 |
if (summary.Error) nextSteps.push('Re-run the audit as administrator/root so that every check can be read.');
|
| 177 |
for (const r of ranked.slice(0, 3)) {
|
|
|
|
| 58 |
const sentence = text => { const t = text.trim(); return !t || /[.!?]$/.test(t) ? t : `${t}.`; };
|
| 59 |
|
| 60 |
// --- Task A: finding ----------------------------------------------------------
|
| 61 |
+
export const EVIDENCE_TEXT = { 'self-reported': 'self-reported by the user', detected: 'detected by the browser' };
|
| 62 |
+
export const SELF_REPORTED = 'Self-reported by the user, not verified on the device.';
|
| 63 |
+
export function evidenceCounts(results) {
|
| 64 |
+
return [results.filter(r => r.evidence === 'detected').length, results.filter(r => r.evidence === 'self-reported').length];
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
export function findingPrompt(record, result, lib) {
|
| 68 |
const osFamily = record.host.os_family;
|
| 69 |
const baseline = record.baseline;
|
|
|
|
| 78 |
`REQUIRED: ${result.expected}`,
|
| 79 |
];
|
| 80 |
if (result.detail) lines.push(`DETAIL: ${result.detail}`);
|
| 81 |
+
if (result.evidence) lines.push(`EVIDENCE: ${EVIDENCE_TEXT[result.evidence]}`);
|
| 82 |
lines.push('REFERENCES:');
|
| 83 |
for (const [label, title, summary] of lib.references(result.id)) lines.push(`- ${label} ${title}: ${summary}`);
|
| 84 |
lines.push('GUIDANCE:');
|
|
|
|
| 133 |
risk = { level: 'None', reason: 'Not applicable to this device.' };
|
| 134 |
recommendation = [];
|
| 135 |
}
|
| 136 |
+
if (result.evidence === 'self-reported') finding = `${finding.replace('The device reports', 'The user reports')} ${SELF_REPORTED}`;
|
| 137 |
const answer = { check_id: result.id, status, title, finding: finding.trim(), risk, recommendation, references };
|
| 138 |
if (verification) answer.verification = verification;
|
| 139 |
+
if (result.evidence) answer.evidence = result.evidence;
|
| 140 |
return answer;
|
| 141 |
}
|
| 142 |
|
|
|
|
| 151 |
const { results, summary, host } = record;
|
| 152 |
const lines = ['TASK: summary', `DEVICE: ${deviceLine(record)}`, `HOSTNAME: ${host.hostname}`,
|
| 153 |
`AUDIT RUN AS ADMIN: ${record.audit.is_admin ? 'yes' : 'no'}`,
|
| 154 |
+
'COUNTS: ' + Object.entries(summary).map(([k, v]) => `${k}=${v}`).join(', ')];
|
| 155 |
+
const [detected, reported] = evidenceCounts(results);
|
| 156 |
+
if (detected || reported) lines.push(`EVIDENCE: ${detected} detected by the browser, ${reported} self-reported by the user`);
|
| 157 |
+
lines.push('RESULTS (id | name | status | found):');
|
| 158 |
for (const r of results) lines.push(`- ${r.id} | ${r.name} | ${r.status} | ${r.actual}`);
|
| 159 |
lines.push('RISK LEVELS (from guidance):');
|
| 160 |
for (const r of results.filter(x => x.status === 'Non-Compliant')) lines.push(`- ${r.id}: ${lib.guide(r.id).risk.level}`);
|
|
|
|
| 184 |
}
|
| 185 |
if (summary.Error) text += ` ${summary.Error} checks could not be read` + (record.audit.is_admin ? '.' : ' because the audit was not run as administrator.');
|
| 186 |
if (summary.Pending) text += ` ${summary.Pending} items are pending.`;
|
| 187 |
+
const reported = evidenceCounts(results)[1];
|
| 188 |
+
if (reported) text += ` ${reported} results are self-reported by the user and were not verified on the device.`;
|
| 189 |
const nextSteps = [];
|
| 190 |
if (summary.Error) nextSteps.push('Re-run the audit as administrator/root so that every check can be read.');
|
| 191 |
for (const r of ranked.slice(0, 3)) {
|
app/renderer/model-core.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Loads the auditor model in WebLLM (runs on the device's GPU through WebGPU), for the desktop app and the phone page.
|
| 2 |
+
// The model is downloaded once and cached by the browser, so later runs work offline.
|
| 3 |
+
|
| 4 |
+
// Until the fine-tuned Devseis model is converted to MLC format and published, the matching base model
|
| 5 |
+
// is used. To switch, set CUSTOM_MODEL to:
|
| 6 |
+
// { model: 'https://huggingface.co/Devseis/<repo>', model_id: 'devseis-auditor-0.5b-q4f16_1', model_lib: '<Qwen2.5-0.5B q4f16_1 WebGPU lib URL from prebuiltAppConfig>' }
|
| 7 |
+
const CUSTOM_MODEL = null;
|
| 8 |
+
// q4f16 needs 16-bit GPU maths (shader-f16); many older and integrated GPUs only support the q4f32 build.
|
| 9 |
+
const BASE_MODEL = { f16: 'Qwen2.5-0.5B-Instruct-q4f16_1-MLC', f32: 'Qwen2.5-0.5B-Instruct-q4f32_1-MLC' };
|
| 10 |
+
|
| 11 |
+
// webllm: the @mlc-ai/web-llm module (bundled in the app, from a CDN on the phone page).
|
| 12 |
+
export async function loadModelWith(webllm, onProgress) {
|
| 13 |
+
const { CreateMLCEngine, prebuiltAppConfig } = webllm;
|
| 14 |
+
if (!('gpu' in navigator)) throw new Error('WebGPU is not available on this device.');
|
| 15 |
+
const adapter = await navigator.gpu.requestAdapter();
|
| 16 |
+
if (!adapter) throw new Error('No WebGPU-capable graphics adapter was found.');
|
| 17 |
+
|
| 18 |
+
let appConfig = prebuiltAppConfig;
|
| 19 |
+
let modelId;
|
| 20 |
+
if (CUSTOM_MODEL) {
|
| 21 |
+
appConfig = { ...prebuiltAppConfig, model_list: [...prebuiltAppConfig.model_list, CUSTOM_MODEL] };
|
| 22 |
+
modelId = CUSTOM_MODEL.model_id;
|
| 23 |
+
} else {
|
| 24 |
+
const wanted = adapter.features.has('shader-f16') ? BASE_MODEL.f16 : BASE_MODEL.f32;
|
| 25 |
+
const entry = prebuiltAppConfig.model_list.find(m => m.model_id === wanted);
|
| 26 |
+
if (!entry) throw new Error('The base model is not in this WebLLM version.');
|
| 27 |
+
modelId = entry.model_id;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
const engine = await CreateMLCEngine(modelId, {
|
| 31 |
+
appConfig,
|
| 32 |
+
initProgressCallback: report => onProgress({ progress: report.progress, text: report.text }),
|
| 33 |
+
});
|
| 34 |
+
return {
|
| 35 |
+
name: modelId,
|
| 36 |
+
fineTuned: Boolean(CUSTOM_MODEL),
|
| 37 |
+
async generate(system, prompt) {
|
| 38 |
+
const reply = await engine.chat.completions.create({
|
| 39 |
+
messages: [{ role: 'system', content: system }, { role: 'user', content: prompt }],
|
| 40 |
+
temperature: 0,
|
| 41 |
+
max_tokens: 900,
|
| 42 |
+
response_format: { type: 'json_object' },
|
| 43 |
+
});
|
| 44 |
+
return reply.choices[0].message.content;
|
| 45 |
+
},
|
| 46 |
+
};
|
| 47 |
+
}
|
app/renderer/model.js
CHANGED
|
@@ -1,46 +1,5 @@
|
|
| 1 |
-
//
|
| 2 |
-
|
| 3 |
-
import {
|
| 4 |
|
| 5 |
-
|
| 6 |
-
// is used. To switch, set CUSTOM_MODEL to:
|
| 7 |
-
// { model: 'https://huggingface.co/Devseis/<repo>', model_id: 'devseis-auditor-0.5b-q4f16_1', model_lib: '<Qwen2.5-0.5B q4f16_1 WebGPU lib URL from prebuiltAppConfig>' }
|
| 8 |
-
const CUSTOM_MODEL = null;
|
| 9 |
-
// q4f16 needs 16-bit GPU maths (shader-f16); many older and integrated GPUs only support the q4f32 build.
|
| 10 |
-
const BASE_MODEL = { f16: 'Qwen2.5-0.5B-Instruct-q4f16_1-MLC', f32: 'Qwen2.5-0.5B-Instruct-q4f32_1-MLC' };
|
| 11 |
-
|
| 12 |
-
export async function loadModel(onProgress) {
|
| 13 |
-
if (!('gpu' in navigator)) throw new Error('WebGPU is not available on this computer.');
|
| 14 |
-
const adapter = await navigator.gpu.requestAdapter();
|
| 15 |
-
if (!adapter) throw new Error('No WebGPU-capable graphics adapter was found.');
|
| 16 |
-
|
| 17 |
-
let appConfig = prebuiltAppConfig;
|
| 18 |
-
let modelId;
|
| 19 |
-
if (CUSTOM_MODEL) {
|
| 20 |
-
appConfig = { ...prebuiltAppConfig, model_list: [...prebuiltAppConfig.model_list, CUSTOM_MODEL] };
|
| 21 |
-
modelId = CUSTOM_MODEL.model_id;
|
| 22 |
-
} else {
|
| 23 |
-
const wanted = adapter.features.has('shader-f16') ? BASE_MODEL.f16 : BASE_MODEL.f32;
|
| 24 |
-
const entry = prebuiltAppConfig.model_list.find(m => m.model_id === wanted);
|
| 25 |
-
if (!entry) throw new Error('The base model is not in this WebLLM version.');
|
| 26 |
-
modelId = entry.model_id;
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
const engine = await CreateMLCEngine(modelId, {
|
| 30 |
-
appConfig,
|
| 31 |
-
initProgressCallback: report => onProgress({ progress: report.progress, text: report.text }),
|
| 32 |
-
});
|
| 33 |
-
return {
|
| 34 |
-
name: modelId,
|
| 35 |
-
fineTuned: Boolean(CUSTOM_MODEL),
|
| 36 |
-
async generate(system, prompt) {
|
| 37 |
-
const reply = await engine.chat.completions.create({
|
| 38 |
-
messages: [{ role: 'system', content: system }, { role: 'user', content: prompt }],
|
| 39 |
-
temperature: 0,
|
| 40 |
-
max_tokens: 900,
|
| 41 |
-
response_format: { type: 'json_object' },
|
| 42 |
-
});
|
| 43 |
-
return reply.choices[0].message.content;
|
| 44 |
-
},
|
| 45 |
-
};
|
| 46 |
-
}
|
|
|
|
| 1 |
+
// Desktop app: WebLLM from the app's own node_modules (works offline once the model is cached).
|
| 2 |
+
import * as webllm from '../node_modules/@mlc-ai/web-llm/lib/index.js';
|
| 3 |
+
import { loadModelWith } from './model-core.js';
|
| 4 |
|
| 5 |
+
export const loadModel = onProgress => loadModelWith(webllm, onProgress);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/renderer/report.js
CHANGED
|
@@ -10,6 +10,8 @@ export function buildReportHtml({ evidence, findings, summary, ai, sha256, model
|
|
| 10 |
const host = evidence.host;
|
| 11 |
const started = evidence.audit.started_at.replace('T', ' ').slice(0, 19);
|
| 12 |
const counts = evidence.summary;
|
|
|
|
|
|
|
| 13 |
|
| 14 |
const sectionHtml = ['baseline', 'gdpr', 'aiact', 'org'].map(sectionId => {
|
| 15 |
const rows = evidence.results.filter(r => r.section === sectionId).map(r => {
|
|
@@ -24,7 +26,8 @@ export function buildReportHtml({ evidence, findings, summary, ai, sha256, model
|
|
| 24 |
<p class="refs">${f.references.map(esc).join(' · ')}</p>
|
| 25 |
</td></tr>`;
|
| 26 |
}
|
| 27 |
-
|
|
|
|
| 28 |
}).join('');
|
| 29 |
return `<h2>${esc(SECTION_TITLES[sectionId])}</h2>
|
| 30 |
<table><thead><tr><th style="width:30%">Check</th><th style="width:14%">Status</th><th>Found</th><th style="width:20%">Required</th></tr></thead><tbody>${rows}</tbody></table>`;
|
|
@@ -64,9 +67,11 @@ export function buildReportHtml({ evidence, findings, summary, ai, sha256, model
|
|
| 64 |
<h1>${esc(host.hostname)}</h1>
|
| 65 |
<div class="meta">
|
| 66 |
<b>Operating system</b><span>${esc(host.os_name)} ${esc(host.os_version)}${host.build ? ` (build ${esc(host.build)})` : ''}</span>
|
| 67 |
-
<b>
|
|
|
|
| 68 |
<b>Audit date</b><span>${esc(started)}</span>
|
| 69 |
-
<b>
|
|
|
|
| 70 |
</div>
|
| 71 |
<div class="score"><div class="big">${summary.score.percent}%</div>
|
| 72 |
<div>${summary.score.compliant} of ${summary.score.assessed} assessed checks compliant<br>
|
|
@@ -82,11 +87,15 @@ ${summary.top_gaps.length ? `<h2>TOP GAPS</h2><ul class="gaps">${summary.top_gap
|
|
| 82 |
${sectionHtml}
|
| 83 |
${aiHtml}
|
| 84 |
|
| 85 |
-
<div class="note">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
Devseis baseline. It supports ISO 27001 internal audits, GDPR Article 32 and EU AI Act deployer duties; it is not a
|
| 87 |
-
certification and not legal advice. Organisational controls reflect the answers given in organisational.conf.</div>
|
| 88 |
<div class="foot">Evidence SHA-256: ${esc(sha256)}<br>
|
| 89 |
Findings written by: ${esc(model || 'built-in report writer')} (${sources.model} by the AI model, ${sources.template} by the built-in writer)<br>
|
| 90 |
-
Collector ${esc(evidence.source.collector)} ${esc(evidence.source.collector_version)} · catalog ${esc(evidence.catalog_version)}</div>
|
| 91 |
</body></html>`;
|
| 92 |
}
|
|
|
|
| 10 |
const host = evidence.host;
|
| 11 |
const started = evidence.audit.started_at.replace('T', ' ').slice(0, 19);
|
| 12 |
const counts = evidence.summary;
|
| 13 |
+
const phone = evidence.source.kind === 'web' || ['ios', 'android'].includes(host.os_family);
|
| 14 |
+
const EVIDENCE_LABEL = { detected: 'detected', 'self-reported': 'self-reported' };
|
| 15 |
|
| 16 |
const sectionHtml = ['baseline', 'gdpr', 'aiact', 'org'].map(sectionId => {
|
| 17 |
const rows = evidence.results.filter(r => r.section === sectionId).map(r => {
|
|
|
|
| 26 |
<p class="refs">${f.references.map(esc).join(' · ')}</p>
|
| 27 |
</td></tr>`;
|
| 28 |
}
|
| 29 |
+
const source = r.evidence ? `<br><span class="muted">${esc(EVIDENCE_LABEL[r.evidence])}</span>` : '';
|
| 30 |
+
return `<tr><td>${esc(r.name)}${source}</td><td>${badge(r.status, STATUS_COLOR[r.status])}</td><td>${esc(r.actual)}</td><td>${esc(r.expected)}</td></tr>${detail}`;
|
| 31 |
}).join('');
|
| 32 |
return `<h2>${esc(SECTION_TITLES[sectionId])}</h2>
|
| 33 |
<table><thead><tr><th style="width:30%">Check</th><th style="width:14%">Status</th><th>Found</th><th style="width:20%">Required</th></tr></thead><tbody>${rows}</tbody></table>`;
|
|
|
|
| 67 |
<h1>${esc(host.hostname)}</h1>
|
| 68 |
<div class="meta">
|
| 69 |
<b>Operating system</b><span>${esc(host.os_name)} ${esc(host.os_version)}${host.build ? ` (build ${esc(host.build)})` : ''}</span>
|
| 70 |
+
${phone ? `<b>Device</b><span>${esc(host.device_model || '')}</span>
|
| 71 |
+
<b>Browser</b><span>${esc(host.browser || '')}</span>` : `<b>Serial number</b><span>${esc(host.serial_number || '')}</span>`}
|
| 72 |
<b>Audit date</b><span>${esc(started)}</span>
|
| 73 |
+
${phone ? `<b>Checked by</b><span>Phone self-check in the browser: ${evidence.results.filter(r => r.evidence === 'detected').length} results detected, ${evidence.results.filter(r => r.evidence === 'self-reported').length} self-reported</span>`
|
| 74 |
+
: `<b>Run as</b><span>${esc(evidence.audit.execution_user)}${evidence.audit.is_admin ? ' (administrator)' : ' (not administrator — some checks could not be read)'}</span>`}
|
| 75 |
</div>
|
| 76 |
<div class="score"><div class="big">${summary.score.percent}%</div>
|
| 77 |
<div>${summary.score.compliant} of ${summary.score.assessed} assessed checks compliant<br>
|
|
|
|
| 87 |
${sectionHtml}
|
| 88 |
${aiHtml}
|
| 89 |
|
| 90 |
+
<div class="note">${phone
|
| 91 |
+
? `This report records what the browser detected on this phone and the answers given by its user at the audit date, and compares them with the
|
| 92 |
+
Devseis baseline. Results marked self-reported were not verified on the device; a browser cannot read a phone's security settings.
|
| 93 |
+
It supports ISO 27001 internal audits, GDPR Article 32 and EU AI Act deployer duties; it is not a certification and not legal advice.`
|
| 94 |
+
: `This report records the settings found on this computer at the audit date and compares them with the
|
| 95 |
Devseis baseline. It supports ISO 27001 internal audits, GDPR Article 32 and EU AI Act deployer duties; it is not a
|
| 96 |
+
certification and not legal advice. Organisational controls reflect the answers given in organisational.conf.`}</div>
|
| 97 |
<div class="foot">Evidence SHA-256: ${esc(sha256)}<br>
|
| 98 |
Findings written by: ${esc(model || 'built-in report writer')} (${sources.model} by the AI model, ${sources.template} by the built-in writer)<br>
|
| 99 |
+
${phone ? 'Phone self-check' : 'Collector'} ${esc(evidence.source.collector)} ${esc(evidence.source.collector_version)} · catalog ${esc(evidence.catalog_version)}</div>
|
| 100 |
</body></html>`;
|
| 101 |
}
|
app/renderer/vuln-core.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Vulnerability matching rules shared by the desktop app (vulndb.cjs) and the phone web check.
|
| 2 |
+
// Pure functions only: no file, network or DOM access, so it runs in Node and in browsers.
|
| 3 |
+
// Matching is exact version comparison; the language model never decides whether a CVE applies.
|
| 4 |
+
export const MAX_AGE_DAYS = 14;
|
| 5 |
+
const SEVERE = new Set(['critical', 'high']);
|
| 6 |
+
|
| 7 |
+
// --- Version comparison ----------------------------------------------------------------------
|
| 8 |
+
// Debian policy 5.6.12: [epoch:]upstream[-revision]; letters sort before non-letters, '~' before everything.
|
| 9 |
+
function dpkgOrder(c) {
|
| 10 |
+
if (c === '~') return -1;
|
| 11 |
+
if (/\d/.test(c)) return 0;
|
| 12 |
+
if (!c) return 0;
|
| 13 |
+
if (/[A-Za-z]/.test(c)) return c.charCodeAt(0);
|
| 14 |
+
return c.charCodeAt(0) + 256;
|
| 15 |
+
}
|
| 16 |
+
function dpkgPart(a, b) {
|
| 17 |
+
let i = 0, j = 0;
|
| 18 |
+
while (i < a.length || j < b.length) {
|
| 19 |
+
let diff = 0;
|
| 20 |
+
while ((i < a.length && !/\d/.test(a[i])) || (j < b.length && !/\d/.test(b[j]))) {
|
| 21 |
+
const ac = dpkgOrder(a[i] || ''), bc = dpkgOrder(b[j] || '');
|
| 22 |
+
if (ac !== bc) return ac - bc;
|
| 23 |
+
i++; j++;
|
| 24 |
+
}
|
| 25 |
+
while (a[i] === '0') i++;
|
| 26 |
+
while (b[j] === '0') j++;
|
| 27 |
+
while (/\d/.test(a[i] || '') && /\d/.test(b[j] || '')) {
|
| 28 |
+
if (!diff) diff = a.charCodeAt(i) - b.charCodeAt(j);
|
| 29 |
+
i++; j++;
|
| 30 |
+
}
|
| 31 |
+
if (/\d/.test(a[i] || '')) return 1;
|
| 32 |
+
if (/\d/.test(b[j] || '')) return -1;
|
| 33 |
+
if (diff) return diff;
|
| 34 |
+
}
|
| 35 |
+
return 0;
|
| 36 |
+
}
|
| 37 |
+
function splitDeb(v) {
|
| 38 |
+
let epoch = 0, rest = v;
|
| 39 |
+
const colon = v.indexOf(':');
|
| 40 |
+
if (colon > 0) { epoch = parseInt(v.slice(0, colon), 10) || 0; rest = v.slice(colon + 1); }
|
| 41 |
+
const dash = rest.lastIndexOf('-');
|
| 42 |
+
return dash > 0 ? [epoch, rest.slice(0, dash), rest.slice(dash + 1)] : [epoch, rest, ''];
|
| 43 |
+
}
|
| 44 |
+
export function dpkgCompare(a, b) {
|
| 45 |
+
const [ea, ua, ra] = splitDeb(a), [eb, ub, rb] = splitDeb(b);
|
| 46 |
+
if (ea !== eb) return ea - eb;
|
| 47 |
+
return dpkgPart(ua, ub) || dpkgPart(ra, rb);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
// rpmvercmp, with epoch:version-release handling.
|
| 51 |
+
function rpmSegment(a, b) {
|
| 52 |
+
if (a === b) return 0;
|
| 53 |
+
const sa = a.match(/~|\^|[A-Za-z]+|\d+/g) || [], sb = b.match(/~|\^|[A-Za-z]+|\d+/g) || [];
|
| 54 |
+
for (let k = 0; k < Math.max(sa.length, sb.length); k++) {
|
| 55 |
+
const x = sa[k], y = sb[k];
|
| 56 |
+
if (x === '~' || y === '~') { if (x !== y) return x === '~' ? -1 : 1; continue; }
|
| 57 |
+
if (x === '^' || y === '^') {
|
| 58 |
+
if (x === undefined) return -1;
|
| 59 |
+
if (y === undefined) return 1;
|
| 60 |
+
if (x !== y) return x === '^' ? 1 : -1;
|
| 61 |
+
continue;
|
| 62 |
+
}
|
| 63 |
+
if (x === undefined) return -1;
|
| 64 |
+
if (y === undefined) return 1;
|
| 65 |
+
const xn = /^\d/.test(x), yn = /^\d/.test(y);
|
| 66 |
+
if (xn !== yn) return xn ? 1 : -1;
|
| 67 |
+
if (xn) {
|
| 68 |
+
const xs = x.replace(/^0+/, ''), ys = y.replace(/^0+/, '');
|
| 69 |
+
if (xs.length !== ys.length) return xs.length - ys.length;
|
| 70 |
+
if (xs !== ys) return xs < ys ? -1 : 1;
|
| 71 |
+
} else if (x !== y) return x < y ? -1 : 1;
|
| 72 |
+
}
|
| 73 |
+
return 0;
|
| 74 |
+
}
|
| 75 |
+
export function splitRpm(v) {
|
| 76 |
+
let epoch = 0, rest = v;
|
| 77 |
+
const colon = v.indexOf(':');
|
| 78 |
+
if (colon > 0) { epoch = parseInt(v.slice(0, colon), 10) || 0; rest = v.slice(colon + 1); }
|
| 79 |
+
const dash = rest.lastIndexOf('-');
|
| 80 |
+
return dash > 0 ? [epoch, rest.slice(0, dash), rest.slice(dash + 1)] : [epoch, rest, ''];
|
| 81 |
+
}
|
| 82 |
+
export function rpmCompare(a, b) {
|
| 83 |
+
const [ea, va, ra] = splitRpm(a), [eb, vb, rb] = splitRpm(b);
|
| 84 |
+
if (ea !== eb) return ea - eb;
|
| 85 |
+
return rpmSegment(va, vb) || rpmSegment(ra, rb);
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
// Desktop application versions: compare the numbers ("6.2.5 (42037)" -> 6.2.5.42037 is avoided by
|
| 89 |
+
// cutting at the first space or parenthesis).
|
| 90 |
+
function appNumbers(v) { return (String(v).split(/[\s(]/)[0].match(/\d+/g) || []).map(Number); }
|
| 91 |
+
export function appCompare(a, b) {
|
| 92 |
+
const x = appNumbers(a), y = appNumbers(b);
|
| 93 |
+
for (let k = 0; k < Math.max(x.length, y.length); k++) {
|
| 94 |
+
const d = (x[k] || 0) - (y[k] || 0);
|
| 95 |
+
if (d) return d;
|
| 96 |
+
}
|
| 97 |
+
return 0;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// --- Matching ------------------------------------------------------------------------------------
|
| 101 |
+
// Linux release key in the bundle, from the inventory's os-release fields.
|
| 102 |
+
export function releaseKey(inventory) {
|
| 103 |
+
const id = String(inventory.distro_id || '').toLowerCase();
|
| 104 |
+
const version = String(inventory.version_id || '');
|
| 105 |
+
const major = version.split('.')[0];
|
| 106 |
+
if (id === 'ubuntu') return `ubuntu:${version}`;
|
| 107 |
+
if (id === 'debian') return `debian:${major}`;
|
| 108 |
+
if (id === 'rhel') return `rhel:${major}`;
|
| 109 |
+
if (id === 'almalinux') return `almalinux:${major}`;
|
| 110 |
+
if (id === 'rocky') return `rocky:${major}`;
|
| 111 |
+
return null;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
export function matchLinux(inventory, bundle) {
|
| 115 |
+
const key = releaseKey(inventory);
|
| 116 |
+
const advisories = key && bundle.releases[key];
|
| 117 |
+
if (!advisories) return { covered: false, reason: `${inventory.pretty_name || inventory.distro_id || 'this distribution'} is not in the vulnerability data (covered: ${Object.keys(bundle.releases).join(', ')})` };
|
| 118 |
+
const deb = inventory.kind === 'deb';
|
| 119 |
+
const compare = deb ? dpkgCompare : rpmCompare;
|
| 120 |
+
// Group installed packages by source package: Debian/Ubuntu advisories name source packages; Red Hat-family
|
| 121 |
+
// advisories name binary packages, sometimes only the main one (openssl) when sub-packages (openssl-libs) are installed.
|
| 122 |
+
const installed = new Map();
|
| 123 |
+
for (const pkg of inventory.packages || []) {
|
| 124 |
+
const group = pkg.source || pkg.name;
|
| 125 |
+
const version = deb ? (pkg.source_version || pkg.version) : pkg.version;
|
| 126 |
+
const names = deb ? [group] : [pkg.name, group];
|
| 127 |
+
const prev = installed.get(group);
|
| 128 |
+
if (!prev) installed.set(group, { version, names: new Set(names) });
|
| 129 |
+
else {
|
| 130 |
+
names.forEach(n => prev.names.add(n));
|
| 131 |
+
if (compare(version, prev.version) > 0) prev.version = version;
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
const hits = [];
|
| 135 |
+
for (const [group, { version, names }] of installed) {
|
| 136 |
+
const seen = new Set();
|
| 137 |
+
for (const name of names) {
|
| 138 |
+
for (const [cves, advisory, fixed, severity] of advisories[name] || []) {
|
| 139 |
+
const key = `${cves}|${fixed}`;
|
| 140 |
+
if (seen.has(key)) continue;
|
| 141 |
+
seen.add(key);
|
| 142 |
+
// Red Hat module streams: only compare within the same major version (nodejs 20 fix does not apply to nodejs 22).
|
| 143 |
+
if (!deb && /\.module[+_]/.test(fixed) && splitRpm(fixed)[1].split('.')[0] !== splitRpm(version)[1].split('.')[0]) continue;
|
| 144 |
+
if (compare(version, fixed) < 0) hits.push({ software: group, version, fixed, severity, cves: cves.split('|'), advisory });
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
return { covered: true, source: key, checked: installed.size, hits };
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
export function matchApps(inventory, bundle, osFamily) {
|
| 152 |
+
const hits = [];
|
| 153 |
+
const matched = new Set();
|
| 154 |
+
for (const app of inventory.apps || []) {
|
| 155 |
+
if (!app.version) continue;
|
| 156 |
+
for (const product of bundle.apps) {
|
| 157 |
+
const pattern = product[osFamily];
|
| 158 |
+
if (!pattern || !new RegExp(pattern, 'i').test(app.name)) continue;
|
| 159 |
+
matched.add(product.id);
|
| 160 |
+
for (const [cve, severity, si, se, ei, ee, exact] of product.cves) {
|
| 161 |
+
const c = bound => appCompare(app.version, bound);
|
| 162 |
+
const affected = exact ? c(exact) === 0
|
| 163 |
+
: (!si || c(si) >= 0) && (!se || c(se) > 0) && (!ei || c(ei) <= 0) && (!ee || c(ee) < 0);
|
| 164 |
+
if (affected) hits.push({ software: app.name, version: app.version, fixed: ee ? ee : '', severity, cves: [cve], advisory: '' });
|
| 165 |
+
}
|
| 166 |
+
break;
|
| 167 |
+
}
|
| 168 |
+
}
|
| 169 |
+
return { covered: true, source: 'nvd', checked: (inventory.apps || []).length, recognised: matched.size, hits };
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
// Group hits per software and CVE; flag actively exploited CVEs (CISA KEV) and likely ones (EPSS).
|
| 173 |
+
export function summarise(result, bundle) {
|
| 174 |
+
const kev = new Set(bundle.kev);
|
| 175 |
+
const rank = { critical: 0, high: 1, medium: 2, low: 3, unknown: 4 };
|
| 176 |
+
const perCve = new Map();
|
| 177 |
+
for (const hit of result.hits) {
|
| 178 |
+
for (const cve of hit.cves) {
|
| 179 |
+
const id = `${hit.software}|${cve}`;
|
| 180 |
+
const prev = perCve.get(id);
|
| 181 |
+
if (!prev || rank[hit.severity] < rank[prev.severity]) perCve.set(id, { ...hit, cve, exploited: kev.has(cve), epss: bundle.epss[cve] || 0 });
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
const all = [...perCve.values()];
|
| 185 |
+
const severe = all.filter(h => SEVERE.has(h.severity) || h.exploited);
|
| 186 |
+
const bySoftware = new Map();
|
| 187 |
+
for (const h of severe) {
|
| 188 |
+
const s = bySoftware.get(h.software) || { software: h.software, version: h.version, fixed: '', cves: [], worst: 'unknown', exploited: false, epss: 0 };
|
| 189 |
+
s.cves.push(h.cve);
|
| 190 |
+
if (rank[h.severity] < rank[s.worst]) s.worst = h.severity;
|
| 191 |
+
s.exploited = s.exploited || h.exploited;
|
| 192 |
+
s.epss = Math.max(s.epss, h.epss);
|
| 193 |
+
if (h.fixed && (!s.fixed || appCompare(h.fixed, s.fixed) > 0 || /[~+]/.test(h.fixed))) s.fixed = h.fixed;
|
| 194 |
+
bySoftware.set(h.software, s);
|
| 195 |
+
}
|
| 196 |
+
const groups = [...bySoftware.values()].sort((a, b) => (b.exploited - a.exploited) || (rank[a.worst] - rank[b.worst]) || (b.cves.length - a.cves.length));
|
| 197 |
+
return { severeCount: new Set(severe.map(h => h.cve)).size, otherCount: new Set(all.filter(h => !severe.includes(h)).map(h => h.cve)).size, groups };
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
export function ageDays(isoDate, now) { return Math.floor((now - Date.parse(isoDate)) / 86400000); }
|
| 202 |
+
|
| 203 |
+
// Turn a match into the vuln.known_vulnerabilities result fields (same wording on every platform).
|
| 204 |
+
export function knownVulnResult(matched, inventory, loaded, osFamily, maxSevere, now = Date.now()) {
|
| 205 |
+
const { bundle, manifest } = loaded;
|
| 206 |
+
if (!matched.covered) return { status: 'Pending', actual: 'not covered', detail: `${matched.reason}.` };
|
| 207 |
+
const dataDate = manifest.built_at.slice(0, 10);
|
| 208 |
+
const age = ageDays(manifest.built_at, now);
|
| 209 |
+
const ageNote = age > MAX_AGE_DAYS ? ` Warning: the vulnerability data is ${age} days old; newer vulnerabilities are not included.` : '';
|
| 210 |
+
const s = summarise(matched, bundle);
|
| 211 |
+
const scope = osFamily === 'linux'
|
| 212 |
+
? `${matched.checked} ${inventory.kind === 'deb' ? 'source packages' : 'packages'} checked against ${matched.source} advisories`
|
| 213 |
+
: osFamily === 'ios'
|
| 214 |
+
? `${(inventory.apps || [])[0] ? inventory.apps[0].name + ' ' + inventory.apps[0].version : 'iOS'} checked against NVD iOS records`
|
| 215 |
+
: `${matched.checked} applications inventoried, ${matched.recognised} recognised (${bundle.apps.filter(a => a[osFamily]).length} common applications covered)`;
|
| 216 |
+
const listed = s.groups.slice(0, 5).map(g => {
|
| 217 |
+
const tags = [`${g.cves.length} CVE${g.cves.length > 1 ? 's' : ''}`, g.worst];
|
| 218 |
+
if (g.exploited) tags.push('actively exploited');
|
| 219 |
+
return `${g.software}${g.software.endsWith(g.version) ? '' : ` ${g.version}`} (${tags.join(', ')}${g.fixed ? `; fixed in ${g.fixed}` : ''}; e.g. ${g.cves.slice(0, 2).join(', ')})`;
|
| 220 |
+
});
|
| 221 |
+
return {
|
| 222 |
+
status: s.severeCount <= maxSevere ? 'Compliant' : 'Non-Compliant',
|
| 223 |
+
actual: `${s.severeCount} critical/high`,
|
| 224 |
+
detail: `Matched against offline OSV/NVD data from ${dataDate}. ${scope}.`
|
| 225 |
+
+ (listed.length ? ` ${listed.join('; ')}${s.groups.length > 5 ? `; and ${s.groups.length - 5} more` : ''}.` : '')
|
| 226 |
+
+ ` ${s.otherCount} medium/low.${ageNote}`,
|
| 227 |
+
};
|
| 228 |
+
}
|
app/test/prompt-parity.mjs
CHANGED
|
@@ -9,7 +9,7 @@ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '.
|
|
| 9 |
const read = (...p) => JSON.parse(readFileSync(path.join(root, ...p), 'utf8'));
|
| 10 |
const lib = new Library({ catalog: read('catalog', 'checks.json'), controls: read('library', 'controls.json'),
|
| 11 |
guidance: read('library', 'check_guidance.json'), aiTools: read('library', 'ai_tools.json') });
|
| 12 |
-
const evidence = readFileSync(path.join(root, 'synthetic', process.env.EVIDENCE_DIR || 'out-v0.
|
| 13 |
|
| 14 |
const python = execFileSync('python3', ['-c', `
|
| 15 |
import json, os, sys
|
|
@@ -17,7 +17,7 @@ sys.path.insert(0, "training")
|
|
| 17 |
import build_dataset as b
|
| 18 |
lib = b.Library()
|
| 19 |
out = []
|
| 20 |
-
for line in list(open("synthetic/" + os.environ.get("EVIDENCE_DIR", "out-v0.
|
| 21 |
r = json.loads(line)
|
| 22 |
item = {"findings": [b.build_finding(r, x, lib, __import__("random").Random(0))[0] for x in r["results"]],
|
| 23 |
"summary": b.build_summary(r, lib)[0]}
|
|
|
|
| 9 |
const read = (...p) => JSON.parse(readFileSync(path.join(root, ...p), 'utf8'));
|
| 10 |
const lib = new Library({ catalog: read('catalog', 'checks.json'), controls: read('library', 'controls.json'),
|
| 11 |
guidance: read('library', 'check_guidance.json'), aiTools: read('library', 'ai_tools.json') });
|
| 12 |
+
const evidence = readFileSync(path.join(root, 'synthetic', process.env.EVIDENCE_DIR || 'out-v0.3', 'evidence.jsonl'), 'utf8').trim().split('\n').slice(0, 60).map(JSON.parse);
|
| 13 |
|
| 14 |
const python = execFileSync('python3', ['-c', `
|
| 15 |
import json, os, sys
|
|
|
|
| 17 |
import build_dataset as b
|
| 18 |
lib = b.Library()
|
| 19 |
out = []
|
| 20 |
+
for line in list(open("synthetic/" + os.environ.get("EVIDENCE_DIR", "out-v0.3") + "/evidence.jsonl"))[:60]:
|
| 21 |
r = json.loads(line)
|
| 22 |
item = {"findings": [b.build_finding(r, x, lib, __import__("random").Random(0))[0] for x in r["results"]],
|
| 23 |
"summary": b.build_summary(r, lib)[0]}
|
app/vulndb.cjs
CHANGED
|
@@ -10,103 +10,8 @@ const zlib = require('node:zlib');
|
|
| 10 |
|
| 11 |
// Ed25519 public key that signs bundles (private key stays with Devseis, outside the repository).
|
| 12 |
const PUBLIC_KEY_FILE = path.join(__dirname, 'vulndb-public-key.pem');
|
| 13 |
-
// Where clients download bundles from
|
| 14 |
-
const VULNDB_URL =
|
| 15 |
-
const MAX_AGE_DAYS = 14;
|
| 16 |
-
const SEVERE = new Set(['critical', 'high']);
|
| 17 |
-
|
| 18 |
-
// --- Version comparison ----------------------------------------------------------------------
|
| 19 |
-
// Debian policy 5.6.12: [epoch:]upstream[-revision]; letters sort before non-letters, '~' before everything.
|
| 20 |
-
function dpkgOrder(c) {
|
| 21 |
-
if (c === '~') return -1;
|
| 22 |
-
if (/\d/.test(c)) return 0;
|
| 23 |
-
if (!c) return 0;
|
| 24 |
-
if (/[A-Za-z]/.test(c)) return c.charCodeAt(0);
|
| 25 |
-
return c.charCodeAt(0) + 256;
|
| 26 |
-
}
|
| 27 |
-
function dpkgPart(a, b) {
|
| 28 |
-
let i = 0, j = 0;
|
| 29 |
-
while (i < a.length || j < b.length) {
|
| 30 |
-
let diff = 0;
|
| 31 |
-
while ((i < a.length && !/\d/.test(a[i])) || (j < b.length && !/\d/.test(b[j]))) {
|
| 32 |
-
const ac = dpkgOrder(a[i] || ''), bc = dpkgOrder(b[j] || '');
|
| 33 |
-
if (ac !== bc) return ac - bc;
|
| 34 |
-
i++; j++;
|
| 35 |
-
}
|
| 36 |
-
while (a[i] === '0') i++;
|
| 37 |
-
while (b[j] === '0') j++;
|
| 38 |
-
while (/\d/.test(a[i] || '') && /\d/.test(b[j] || '')) {
|
| 39 |
-
if (!diff) diff = a.charCodeAt(i) - b.charCodeAt(j);
|
| 40 |
-
i++; j++;
|
| 41 |
-
}
|
| 42 |
-
if (/\d/.test(a[i] || '')) return 1;
|
| 43 |
-
if (/\d/.test(b[j] || '')) return -1;
|
| 44 |
-
if (diff) return diff;
|
| 45 |
-
}
|
| 46 |
-
return 0;
|
| 47 |
-
}
|
| 48 |
-
function splitDeb(v) {
|
| 49 |
-
let epoch = 0, rest = v;
|
| 50 |
-
const colon = v.indexOf(':');
|
| 51 |
-
if (colon > 0) { epoch = parseInt(v.slice(0, colon), 10) || 0; rest = v.slice(colon + 1); }
|
| 52 |
-
const dash = rest.lastIndexOf('-');
|
| 53 |
-
return dash > 0 ? [epoch, rest.slice(0, dash), rest.slice(dash + 1)] : [epoch, rest, ''];
|
| 54 |
-
}
|
| 55 |
-
function dpkgCompare(a, b) {
|
| 56 |
-
const [ea, ua, ra] = splitDeb(a), [eb, ub, rb] = splitDeb(b);
|
| 57 |
-
if (ea !== eb) return ea - eb;
|
| 58 |
-
return dpkgPart(ua, ub) || dpkgPart(ra, rb);
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
// rpmvercmp, with epoch:version-release handling.
|
| 62 |
-
function rpmSegment(a, b) {
|
| 63 |
-
if (a === b) return 0;
|
| 64 |
-
const sa = a.match(/~|\^|[A-Za-z]+|\d+/g) || [], sb = b.match(/~|\^|[A-Za-z]+|\d+/g) || [];
|
| 65 |
-
for (let k = 0; k < Math.max(sa.length, sb.length); k++) {
|
| 66 |
-
const x = sa[k], y = sb[k];
|
| 67 |
-
if (x === '~' || y === '~') { if (x !== y) return x === '~' ? -1 : 1; continue; }
|
| 68 |
-
if (x === '^' || y === '^') {
|
| 69 |
-
if (x === undefined) return -1;
|
| 70 |
-
if (y === undefined) return 1;
|
| 71 |
-
if (x !== y) return x === '^' ? 1 : -1;
|
| 72 |
-
continue;
|
| 73 |
-
}
|
| 74 |
-
if (x === undefined) return -1;
|
| 75 |
-
if (y === undefined) return 1;
|
| 76 |
-
const xn = /^\d/.test(x), yn = /^\d/.test(y);
|
| 77 |
-
if (xn !== yn) return xn ? 1 : -1;
|
| 78 |
-
if (xn) {
|
| 79 |
-
const xs = x.replace(/^0+/, ''), ys = y.replace(/^0+/, '');
|
| 80 |
-
if (xs.length !== ys.length) return xs.length - ys.length;
|
| 81 |
-
if (xs !== ys) return xs < ys ? -1 : 1;
|
| 82 |
-
} else if (x !== y) return x < y ? -1 : 1;
|
| 83 |
-
}
|
| 84 |
-
return 0;
|
| 85 |
-
}
|
| 86 |
-
function splitRpm(v) {
|
| 87 |
-
let epoch = 0, rest = v;
|
| 88 |
-
const colon = v.indexOf(':');
|
| 89 |
-
if (colon > 0) { epoch = parseInt(v.slice(0, colon), 10) || 0; rest = v.slice(colon + 1); }
|
| 90 |
-
const dash = rest.lastIndexOf('-');
|
| 91 |
-
return dash > 0 ? [epoch, rest.slice(0, dash), rest.slice(dash + 1)] : [epoch, rest, ''];
|
| 92 |
-
}
|
| 93 |
-
function rpmCompare(a, b) {
|
| 94 |
-
const [ea, va, ra] = splitRpm(a), [eb, vb, rb] = splitRpm(b);
|
| 95 |
-
if (ea !== eb) return ea - eb;
|
| 96 |
-
return rpmSegment(va, vb) || rpmSegment(ra, rb);
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
// Desktop application versions: compare the numbers ("6.2.5 (42037)" -> 6.2.5.42037 is avoided by
|
| 100 |
-
// cutting at the first space or parenthesis).
|
| 101 |
-
function appNumbers(v) { return (String(v).split(/[\s(]/)[0].match(/\d+/g) || []).map(Number); }
|
| 102 |
-
function appCompare(a, b) {
|
| 103 |
-
const x = appNumbers(a), y = appNumbers(b);
|
| 104 |
-
for (let k = 0; k < Math.max(x.length, y.length); k++) {
|
| 105 |
-
const d = (x[k] || 0) - (y[k] || 0);
|
| 106 |
-
if (d) return d;
|
| 107 |
-
}
|
| 108 |
-
return 0;
|
| 109 |
-
}
|
| 110 |
|
| 111 |
// --- Bundle loading -------------------------------------------------------------------------------
|
| 112 |
function verifyBundle(dir) {
|
|
@@ -163,107 +68,9 @@ function load(dirs) {
|
|
| 163 |
return { error: errors.join('; ') || 'no vulnerability bundle found' };
|
| 164 |
}
|
| 165 |
|
| 166 |
-
// --- Matching --------------------
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
const id = String(inventory.distro_id || '').toLowerCase();
|
| 170 |
-
const version = String(inventory.version_id || '');
|
| 171 |
-
const major = version.split('.')[0];
|
| 172 |
-
if (id === 'ubuntu') return `ubuntu:${version}`;
|
| 173 |
-
if (id === 'debian') return `debian:${major}`;
|
| 174 |
-
if (id === 'rhel') return `rhel:${major}`;
|
| 175 |
-
if (id === 'almalinux') return `almalinux:${major}`;
|
| 176 |
-
if (id === 'rocky') return `rocky:${major}`;
|
| 177 |
-
return null;
|
| 178 |
-
}
|
| 179 |
-
|
| 180 |
-
function matchLinux(inventory, bundle) {
|
| 181 |
-
const key = releaseKey(inventory);
|
| 182 |
-
const advisories = key && bundle.releases[key];
|
| 183 |
-
if (!advisories) return { covered: false, reason: `${inventory.pretty_name || inventory.distro_id || 'this distribution'} is not in the vulnerability data (covered: ${Object.keys(bundle.releases).join(', ')})` };
|
| 184 |
-
const deb = inventory.kind === 'deb';
|
| 185 |
-
const compare = deb ? dpkgCompare : rpmCompare;
|
| 186 |
-
// Group installed packages by source package: Debian/Ubuntu advisories name source packages; Red Hat-family
|
| 187 |
-
// advisories name binary packages, sometimes only the main one (openssl) when sub-packages (openssl-libs) are installed.
|
| 188 |
-
const installed = new Map();
|
| 189 |
-
for (const pkg of inventory.packages || []) {
|
| 190 |
-
const group = pkg.source || pkg.name;
|
| 191 |
-
const version = deb ? (pkg.source_version || pkg.version) : pkg.version;
|
| 192 |
-
const names = deb ? [group] : [pkg.name, group];
|
| 193 |
-
const prev = installed.get(group);
|
| 194 |
-
if (!prev) installed.set(group, { version, names: new Set(names) });
|
| 195 |
-
else {
|
| 196 |
-
names.forEach(n => prev.names.add(n));
|
| 197 |
-
if (compare(version, prev.version) > 0) prev.version = version;
|
| 198 |
-
}
|
| 199 |
-
}
|
| 200 |
-
const hits = [];
|
| 201 |
-
for (const [group, { version, names }] of installed) {
|
| 202 |
-
const seen = new Set();
|
| 203 |
-
for (const name of names) {
|
| 204 |
-
for (const [cves, advisory, fixed, severity] of advisories[name] || []) {
|
| 205 |
-
const key = `${cves}|${fixed}`;
|
| 206 |
-
if (seen.has(key)) continue;
|
| 207 |
-
seen.add(key);
|
| 208 |
-
// Red Hat module streams: only compare within the same major version (nodejs 20 fix does not apply to nodejs 22).
|
| 209 |
-
if (!deb && /\.module[+_]/.test(fixed) && splitRpm(fixed)[1].split('.')[0] !== splitRpm(version)[1].split('.')[0]) continue;
|
| 210 |
-
if (compare(version, fixed) < 0) hits.push({ software: group, version, fixed, severity, cves: cves.split('|'), advisory });
|
| 211 |
-
}
|
| 212 |
-
}
|
| 213 |
-
}
|
| 214 |
-
return { covered: true, source: key, checked: installed.size, hits };
|
| 215 |
-
}
|
| 216 |
-
|
| 217 |
-
function matchApps(inventory, bundle, osFamily) {
|
| 218 |
-
const hits = [];
|
| 219 |
-
const matched = new Set();
|
| 220 |
-
for (const app of inventory.apps || []) {
|
| 221 |
-
if (!app.version) continue;
|
| 222 |
-
for (const product of bundle.apps) {
|
| 223 |
-
const pattern = product[osFamily];
|
| 224 |
-
if (!pattern || !new RegExp(pattern, 'i').test(app.name)) continue;
|
| 225 |
-
matched.add(product.id);
|
| 226 |
-
for (const [cve, severity, si, se, ei, ee, exact] of product.cves) {
|
| 227 |
-
const c = bound => appCompare(app.version, bound);
|
| 228 |
-
const affected = exact ? c(exact) === 0
|
| 229 |
-
: (!si || c(si) >= 0) && (!se || c(se) > 0) && (!ei || c(ei) <= 0) && (!ee || c(ee) < 0);
|
| 230 |
-
if (affected) hits.push({ software: app.name, version: app.version, fixed: ee ? ee : '', severity, cves: [cve], advisory: '' });
|
| 231 |
-
}
|
| 232 |
-
break;
|
| 233 |
-
}
|
| 234 |
-
}
|
| 235 |
-
return { covered: true, source: 'nvd', checked: (inventory.apps || []).length, recognised: matched.size, hits };
|
| 236 |
-
}
|
| 237 |
-
|
| 238 |
-
// Group hits per software and CVE; flag actively exploited CVEs (CISA KEV) and likely ones (EPSS).
|
| 239 |
-
function summarise(result, bundle) {
|
| 240 |
-
const kev = new Set(bundle.kev);
|
| 241 |
-
const rank = { critical: 0, high: 1, medium: 2, low: 3, unknown: 4 };
|
| 242 |
-
const perCve = new Map();
|
| 243 |
-
for (const hit of result.hits) {
|
| 244 |
-
for (const cve of hit.cves) {
|
| 245 |
-
const id = `${hit.software}|${cve}`;
|
| 246 |
-
const prev = perCve.get(id);
|
| 247 |
-
if (!prev || rank[hit.severity] < rank[prev.severity]) perCve.set(id, { ...hit, cve, exploited: kev.has(cve), epss: bundle.epss[cve] || 0 });
|
| 248 |
-
}
|
| 249 |
-
}
|
| 250 |
-
const all = [...perCve.values()];
|
| 251 |
-
const severe = all.filter(h => SEVERE.has(h.severity) || h.exploited);
|
| 252 |
-
const bySoftware = new Map();
|
| 253 |
-
for (const h of severe) {
|
| 254 |
-
const s = bySoftware.get(h.software) || { software: h.software, version: h.version, fixed: '', cves: [], worst: 'unknown', exploited: false, epss: 0 };
|
| 255 |
-
s.cves.push(h.cve);
|
| 256 |
-
if (rank[h.severity] < rank[s.worst]) s.worst = h.severity;
|
| 257 |
-
s.exploited = s.exploited || h.exploited;
|
| 258 |
-
s.epss = Math.max(s.epss, h.epss);
|
| 259 |
-
if (h.fixed && (!s.fixed || appCompare(h.fixed, s.fixed) > 0 || /[~+]/.test(h.fixed))) s.fixed = h.fixed;
|
| 260 |
-
bySoftware.set(h.software, s);
|
| 261 |
-
}
|
| 262 |
-
const groups = [...bySoftware.values()].sort((a, b) => (b.exploited - a.exploited) || (rank[a.worst] - rank[b.worst]) || (b.cves.length - a.cves.length));
|
| 263 |
-
return { severeCount: new Set(severe.map(h => h.cve)).size, otherCount: new Set(all.filter(h => !severe.includes(h)).map(h => h.cve)).size, groups };
|
| 264 |
-
}
|
| 265 |
-
|
| 266 |
-
function ageDays(isoDate, now) { return Math.floor((now - Date.parse(isoDate)) / 86400000); }
|
| 267 |
|
| 268 |
// Replace the collector's "Pending" vuln.known_vulnerabilities result with the matched one.
|
| 269 |
function apply(evidence, inventory, loaded, maxSevere, now = Date.now()) {
|
|
@@ -274,35 +81,12 @@ function apply(evidence, inventory, loaded, maxSevere, now = Date.now()) {
|
|
| 274 |
result.detail = `${result.detail || ''} No vulnerability data available: ${loaded.error}.`.trim();
|
| 275 |
return evidence;
|
| 276 |
}
|
| 277 |
-
const { bundle, manifest } = loaded;
|
| 278 |
-
const dataDate = manifest.built_at.slice(0, 10);
|
| 279 |
-
const age = ageDays(manifest.built_at, now);
|
| 280 |
-
const ageNote = age > MAX_AGE_DAYS ? ` Warning: the vulnerability data is ${age} days old; newer vulnerabilities are not included.` : '';
|
| 281 |
const osFamily = evidence.host && evidence.host.os_family;
|
| 282 |
-
const matched = osFamily === 'linux' ? matchLinux(inventory, bundle) : matchApps(inventory, bundle, osFamily);
|
| 283 |
-
|
| 284 |
-
result.status = 'Pending';
|
| 285 |
-
result.actual = 'not covered';
|
| 286 |
-
result.detail = `${matched.reason}.`;
|
| 287 |
-
return evidence;
|
| 288 |
-
}
|
| 289 |
-
const s = summarise(matched, bundle);
|
| 290 |
-
const scope = osFamily === 'linux'
|
| 291 |
-
? `${matched.checked} ${inventory.kind === 'deb' ? 'source packages' : 'packages'} checked against ${matched.source} advisories`
|
| 292 |
-
: `${matched.checked} applications inventoried, ${matched.recognised} recognised (${bundle.apps.length} common applications covered)`;
|
| 293 |
-
const listed = s.groups.slice(0, 5).map(g => {
|
| 294 |
-
const tags = [`${g.cves.length} CVE${g.cves.length > 1 ? 's' : ''}`, g.worst];
|
| 295 |
-
if (g.exploited) tags.push('actively exploited');
|
| 296 |
-
return `${g.software} ${g.version} (${tags.join(', ')}${g.fixed ? `; fixed in ${g.fixed}` : ''}; e.g. ${g.cves.slice(0, 2).join(', ')})`;
|
| 297 |
-
});
|
| 298 |
-
result.actual = `${s.severeCount} critical/high`;
|
| 299 |
-
result.status = s.severeCount <= maxSevere ? 'Compliant' : 'Non-Compliant';
|
| 300 |
-
result.detail = `Matched against offline OSV/NVD data from ${dataDate}. ${scope}.`
|
| 301 |
-
+ (listed.length ? ` ${listed.join('; ')}${s.groups.length > 5 ? `; and ${s.groups.length - 5} more` : ''}.` : '')
|
| 302 |
-
+ ` ${s.otherCount} medium/low.${ageNote}`;
|
| 303 |
evidence.summary = { Compliant: 0, 'Non-Compliant': 0, NotApplicable: 0, Pending: 0, Error: 0 };
|
| 304 |
for (const r of evidence.results) evidence.summary[r.status] += 1;
|
| 305 |
return evidence;
|
| 306 |
}
|
| 307 |
|
| 308 |
-
module.exports = { dpkgCompare, rpmCompare, appCompare, verifyBundle, refresh, load, releaseKey, matchLinux, matchApps, summarise, apply, MAX_AGE_DAYS };
|
|
|
|
| 10 |
|
| 11 |
// Ed25519 public key that signs bundles (private key stays with Devseis, outside the repository).
|
| 12 |
const PUBLIC_KEY_FILE = path.join(__dirname, 'vulndb-public-key.pem');
|
| 13 |
+
// Where clients download bundles from (public Hugging Face dataset).
|
| 14 |
+
const VULNDB_URL = 'https://huggingface.co/datasets/Devseis/endpoint-auditor-vulndb/resolve/main';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
// --- Bundle loading -------------------------------------------------------------------------------
|
| 17 |
function verifyBundle(dir) {
|
|
|
|
| 68 |
return { error: errors.join('; ') || 'no vulnerability bundle found' };
|
| 69 |
}
|
| 70 |
|
| 71 |
+
// --- Matching (rules in renderer/vuln-core.js, shared with the phone web check) -------------------
|
| 72 |
+
const core = require('./renderer/vuln-core.js');
|
| 73 |
+
const { dpkgCompare, rpmCompare, appCompare, releaseKey, matchLinux, matchApps, summarise, knownVulnResult } = core;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
// Replace the collector's "Pending" vuln.known_vulnerabilities result with the matched one.
|
| 76 |
function apply(evidence, inventory, loaded, maxSevere, now = Date.now()) {
|
|
|
|
| 81 |
result.detail = `${result.detail || ''} No vulnerability data available: ${loaded.error}.`.trim();
|
| 82 |
return evidence;
|
| 83 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
const osFamily = evidence.host && evidence.host.os_family;
|
| 85 |
+
const matched = osFamily === 'linux' ? matchLinux(inventory, loaded.bundle) : matchApps(inventory, loaded.bundle, osFamily);
|
| 86 |
+
Object.assign(result, knownVulnResult(matched, inventory, loaded, osFamily, maxSevere, now));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
evidence.summary = { Compliant: 0, 'Non-Compliant': 0, NotApplicable: 0, Pending: 0, Error: 0 };
|
| 88 |
for (const r of evidence.results) evidence.summary[r.status] += 1;
|
| 89 |
return evidence;
|
| 90 |
}
|
| 91 |
|
| 92 |
+
module.exports = { dpkgCompare, rpmCompare, appCompare, verifyBundle, refresh, load, releaseKey, matchLinux, matchApps, summarise, apply, MAX_AGE_DAYS: core.MAX_AGE_DAYS };
|
baseline.conf
CHANGED
|
@@ -63,3 +63,9 @@ PERSONAL_DATA_SCAN=0
|
|
| 63 |
# --- EU AI Act -------------------------------------------------------------
|
| 64 |
# AI tools allowed on endpoints, as named in the AI software inventory (e.g. Microsoft Copilot,ChatGPT).
|
| 65 |
APPROVED_AI_TOOLS=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
# --- EU AI Act -------------------------------------------------------------
|
| 64 |
# AI tools allowed on endpoints, as named in the AI software inventory (e.g. Microsoft Copilot,ChatGPT).
|
| 65 |
APPROVED_AI_TOOLS=
|
| 66 |
+
|
| 67 |
+
# Phones (mobile web check)
|
| 68 |
+
IOS_MIN_MAJOR=18
|
| 69 |
+
ANDROID_MIN_MAJOR=13
|
| 70 |
+
MOBILE_PATCH_MAX_AGE_DAYS=90
|
| 71 |
+
MOBILE_PASSCODE_MIN_DIGITS=6
|
catalog/checks.json
CHANGED
|
@@ -1,6 +1,12 @@
|
|
| 1 |
{
|
| 2 |
-
"catalog_version": "0.
|
| 3 |
-
"statuses": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
"status_meaning": {
|
| 5 |
"Compliant": "The device meets the baseline value.",
|
| 6 |
"Non-Compliant": "The device does not meet the baseline value.",
|
|
@@ -9,10 +15,22 @@
|
|
| 9 |
"Error": "The value could not be read (for example the collector was not run as administrator/root)."
|
| 10 |
},
|
| 11 |
"sections": [
|
| 12 |
-
{
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
],
|
| 17 |
"references": {
|
| 18 |
"iso27001": {
|
|
@@ -71,82 +89,1804 @@
|
|
| 71 |
}
|
| 72 |
},
|
| 73 |
"checks": [
|
| 74 |
-
{
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
{
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
{
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
{
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
]
|
| 152 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"catalog_version": "0.3.0",
|
| 3 |
+
"statuses": [
|
| 4 |
+
"Compliant",
|
| 5 |
+
"Non-Compliant",
|
| 6 |
+
"NotApplicable",
|
| 7 |
+
"Pending",
|
| 8 |
+
"Error"
|
| 9 |
+
],
|
| 10 |
"status_meaning": {
|
| 11 |
"Compliant": "The device meets the baseline value.",
|
| 12 |
"Non-Compliant": "The device does not meet the baseline value.",
|
|
|
|
| 15 |
"Error": "The value could not be read (for example the collector was not run as administrator/root)."
|
| 16 |
},
|
| 17 |
"sections": [
|
| 18 |
+
{
|
| 19 |
+
"id": "baseline",
|
| 20 |
+
"title": "COMPLIANCE RESULTS"
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"id": "gdpr",
|
| 24 |
+
"title": "GDPR TECHNICAL CONTROLS"
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": "aiact",
|
| 28 |
+
"title": "EU AI ACT CONTROLS"
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"id": "org",
|
| 32 |
+
"title": "ORGANISATIONAL CONTROLS"
|
| 33 |
+
}
|
| 34 |
],
|
| 35 |
"references": {
|
| 36 |
"iso27001": {
|
|
|
|
| 89 |
}
|
| 90 |
},
|
| 91 |
"checks": [
|
| 92 |
+
{
|
| 93 |
+
"id": "password.min_length",
|
| 94 |
+
"section": "baseline",
|
| 95 |
+
"name": "Minimum password length",
|
| 96 |
+
"os": [
|
| 97 |
+
"windows",
|
| 98 |
+
"linux",
|
| 99 |
+
"macos"
|
| 100 |
+
],
|
| 101 |
+
"baseline_keys": [
|
| 102 |
+
"PASSWORD_MIN_LENGTH"
|
| 103 |
+
],
|
| 104 |
+
"rule": "Configured minimum length >= PASSWORD_MIN_LENGTH.",
|
| 105 |
+
"controls": {
|
| 106 |
+
"iso27001": [
|
| 107 |
+
"A.5.17",
|
| 108 |
+
"A.8.5"
|
| 109 |
+
],
|
| 110 |
+
"gdpr": [
|
| 111 |
+
"Art. 32(1)(b)"
|
| 112 |
+
],
|
| 113 |
+
"ai_act": []
|
| 114 |
+
}
|
| 115 |
+
},
|
| 116 |
+
{
|
| 117 |
+
"id": "password.history",
|
| 118 |
+
"section": "baseline",
|
| 119 |
+
"name": "Password history",
|
| 120 |
+
"os": [
|
| 121 |
+
"windows",
|
| 122 |
+
"linux",
|
| 123 |
+
"macos"
|
| 124 |
+
],
|
| 125 |
+
"baseline_keys": [
|
| 126 |
+
"PASSWORD_HISTORY"
|
| 127 |
+
],
|
| 128 |
+
"rule": "Remembered passwords >= PASSWORD_HISTORY.",
|
| 129 |
+
"controls": {
|
| 130 |
+
"iso27001": [
|
| 131 |
+
"A.5.17",
|
| 132 |
+
"A.8.5"
|
| 133 |
+
],
|
| 134 |
+
"gdpr": [
|
| 135 |
+
"Art. 32(1)(b)"
|
| 136 |
+
],
|
| 137 |
+
"ai_act": []
|
| 138 |
+
}
|
| 139 |
+
},
|
| 140 |
+
{
|
| 141 |
+
"id": "password.expiration",
|
| 142 |
+
"section": "baseline",
|
| 143 |
+
"name": "Password expiration",
|
| 144 |
+
"os": [
|
| 145 |
+
"windows",
|
| 146 |
+
"linux",
|
| 147 |
+
"macos"
|
| 148 |
+
],
|
| 149 |
+
"baseline_keys": [
|
| 150 |
+
"PASSWORD_MAX_AGE_DAYS"
|
| 151 |
+
],
|
| 152 |
+
"rule": "Passwords expire after 1..PASSWORD_MAX_AGE_DAYS days (0 in the baseline = expiry not required).",
|
| 153 |
+
"controls": {
|
| 154 |
+
"iso27001": [
|
| 155 |
+
"A.5.17"
|
| 156 |
+
],
|
| 157 |
+
"gdpr": [
|
| 158 |
+
"Art. 32(1)(b)"
|
| 159 |
+
],
|
| 160 |
+
"ai_act": []
|
| 161 |
+
}
|
| 162 |
+
},
|
| 163 |
+
{
|
| 164 |
+
"id": "password.min_age",
|
| 165 |
+
"section": "baseline",
|
| 166 |
+
"name": "Minimum password age",
|
| 167 |
+
"os": [
|
| 168 |
+
"windows",
|
| 169 |
+
"linux",
|
| 170 |
+
"macos"
|
| 171 |
+
],
|
| 172 |
+
"baseline_keys": [
|
| 173 |
+
"PASSWORD_MIN_AGE_DAYS"
|
| 174 |
+
],
|
| 175 |
+
"rule": "Minimum age >= PASSWORD_MIN_AGE_DAYS days. macOS has no such setting (NotApplicable).",
|
| 176 |
+
"controls": {
|
| 177 |
+
"iso27001": [
|
| 178 |
+
"A.5.17"
|
| 179 |
+
],
|
| 180 |
+
"gdpr": [
|
| 181 |
+
"Art. 32(1)(b)"
|
| 182 |
+
],
|
| 183 |
+
"ai_act": []
|
| 184 |
+
}
|
| 185 |
+
},
|
| 186 |
+
{
|
| 187 |
+
"id": "password.complexity",
|
| 188 |
+
"section": "baseline",
|
| 189 |
+
"name": "Password complexity",
|
| 190 |
+
"os": [
|
| 191 |
+
"windows",
|
| 192 |
+
"linux",
|
| 193 |
+
"macos"
|
| 194 |
+
],
|
| 195 |
+
"baseline_keys": [
|
| 196 |
+
"PASSWORD_COMPLEXITY"
|
| 197 |
+
],
|
| 198 |
+
"rule": "Complexity enforced (mixed character classes).",
|
| 199 |
+
"controls": {
|
| 200 |
+
"iso27001": [
|
| 201 |
+
"A.5.17",
|
| 202 |
+
"A.8.5"
|
| 203 |
+
],
|
| 204 |
+
"gdpr": [
|
| 205 |
+
"Art. 32(1)(b)"
|
| 206 |
+
],
|
| 207 |
+
"ai_act": []
|
| 208 |
+
}
|
| 209 |
+
},
|
| 210 |
+
{
|
| 211 |
+
"id": "lockout.threshold",
|
| 212 |
+
"section": "baseline",
|
| 213 |
+
"name": "Account lockout threshold",
|
| 214 |
+
"os": [
|
| 215 |
+
"windows",
|
| 216 |
+
"linux",
|
| 217 |
+
"macos"
|
| 218 |
+
],
|
| 219 |
+
"baseline_keys": [
|
| 220 |
+
"LOCKOUT_THRESHOLD_MAX"
|
| 221 |
+
],
|
| 222 |
+
"rule": "Lockout after 1..LOCKOUT_THRESHOLD_MAX failed attempts (0 = never locks).",
|
| 223 |
+
"controls": {
|
| 224 |
+
"iso27001": [
|
| 225 |
+
"A.8.5"
|
| 226 |
+
],
|
| 227 |
+
"gdpr": [
|
| 228 |
+
"Art. 32(1)(b)"
|
| 229 |
+
],
|
| 230 |
+
"ai_act": []
|
| 231 |
+
}
|
| 232 |
+
},
|
| 233 |
+
{
|
| 234 |
+
"id": "lockout.duration",
|
| 235 |
+
"section": "baseline",
|
| 236 |
+
"name": "Account lockout duration",
|
| 237 |
+
"os": [
|
| 238 |
+
"windows",
|
| 239 |
+
"linux",
|
| 240 |
+
"macos"
|
| 241 |
+
],
|
| 242 |
+
"baseline_keys": [
|
| 243 |
+
"LOCKOUT_DURATION_MIN_MINUTES"
|
| 244 |
+
],
|
| 245 |
+
"rule": "Locked for >= LOCKOUT_DURATION_MIN_MINUTES minutes, or until an administrator unlocks.",
|
| 246 |
+
"controls": {
|
| 247 |
+
"iso27001": [
|
| 248 |
+
"A.8.5"
|
| 249 |
+
],
|
| 250 |
+
"gdpr": [
|
| 251 |
+
"Art. 32(1)(b)"
|
| 252 |
+
],
|
| 253 |
+
"ai_act": []
|
| 254 |
+
}
|
| 255 |
+
},
|
| 256 |
+
{
|
| 257 |
+
"id": "lockout.observation",
|
| 258 |
+
"section": "baseline",
|
| 259 |
+
"name": "Lockout observation period",
|
| 260 |
+
"os": [
|
| 261 |
+
"windows",
|
| 262 |
+
"linux",
|
| 263 |
+
"macos"
|
| 264 |
+
],
|
| 265 |
+
"baseline_keys": [
|
| 266 |
+
"LOCKOUT_OBSERVATION_MIN_MINUTES"
|
| 267 |
+
],
|
| 268 |
+
"rule": "Failed-attempt counter kept for >= LOCKOUT_OBSERVATION_MIN_MINUTES minutes.",
|
| 269 |
+
"controls": {
|
| 270 |
+
"iso27001": [
|
| 271 |
+
"A.8.5"
|
| 272 |
+
],
|
| 273 |
+
"gdpr": [
|
| 274 |
+
"Art. 32(1)(b)"
|
| 275 |
+
],
|
| 276 |
+
"ai_act": []
|
| 277 |
+
}
|
| 278 |
+
},
|
| 279 |
+
{
|
| 280 |
+
"id": "malware.antivirus",
|
| 281 |
+
"section": "baseline",
|
| 282 |
+
"name": "Antivirus",
|
| 283 |
+
"os": [
|
| 284 |
+
"windows",
|
| 285 |
+
"linux",
|
| 286 |
+
"macos",
|
| 287 |
+
"android"
|
| 288 |
+
],
|
| 289 |
+
"baseline_keys": [],
|
| 290 |
+
"rule": "An enabled, up-to-date antivirus product is present. Android: Google Play Protect scanning is on.",
|
| 291 |
+
"controls": {
|
| 292 |
+
"iso27001": [
|
| 293 |
+
"A.8.7"
|
| 294 |
+
],
|
| 295 |
+
"gdpr": [
|
| 296 |
+
"Art. 32(1)(b)"
|
| 297 |
+
],
|
| 298 |
+
"ai_act": []
|
| 299 |
+
},
|
| 300 |
+
"names": {
|
| 301 |
+
"android": "Google Play Protect"
|
| 302 |
+
}
|
| 303 |
+
},
|
| 304 |
+
{
|
| 305 |
+
"id": "malware.service",
|
| 306 |
+
"section": "baseline",
|
| 307 |
+
"name": "Malware protection service",
|
| 308 |
+
"names": {
|
| 309 |
+
"windows": "Microsoft Defender Antimalware",
|
| 310 |
+
"linux": "ClamAV malware protection service",
|
| 311 |
+
"macos": "Apple malware protection service"
|
| 312 |
+
},
|
| 313 |
+
"os": [
|
| 314 |
+
"windows",
|
| 315 |
+
"linux",
|
| 316 |
+
"macos"
|
| 317 |
+
],
|
| 318 |
+
"baseline_keys": [],
|
| 319 |
+
"rule": "The platform malware protection service is running.",
|
| 320 |
+
"controls": {
|
| 321 |
+
"iso27001": [
|
| 322 |
+
"A.8.7"
|
| 323 |
+
],
|
| 324 |
+
"gdpr": [
|
| 325 |
+
"Art. 32(1)(b)"
|
| 326 |
+
],
|
| 327 |
+
"ai_act": []
|
| 328 |
+
}
|
| 329 |
+
},
|
| 330 |
+
{
|
| 331 |
+
"id": "malware.realtime",
|
| 332 |
+
"section": "baseline",
|
| 333 |
+
"name": "Real-time protection",
|
| 334 |
+
"os": [
|
| 335 |
+
"windows"
|
| 336 |
+
],
|
| 337 |
+
"baseline_keys": [],
|
| 338 |
+
"rule": "Defender real-time protection is on.",
|
| 339 |
+
"controls": {
|
| 340 |
+
"iso27001": [
|
| 341 |
+
"A.8.7"
|
| 342 |
+
],
|
| 343 |
+
"gdpr": [
|
| 344 |
+
"Art. 32(1)(b)"
|
| 345 |
+
],
|
| 346 |
+
"ai_act": []
|
| 347 |
+
}
|
| 348 |
+
},
|
| 349 |
+
{
|
| 350 |
+
"id": "malware.on_access",
|
| 351 |
+
"section": "baseline",
|
| 352 |
+
"name": "On-access malware protection",
|
| 353 |
+
"names": {
|
| 354 |
+
"windows": "On-access protection"
|
| 355 |
+
},
|
| 356 |
+
"os": [
|
| 357 |
+
"windows",
|
| 358 |
+
"linux",
|
| 359 |
+
"macos"
|
| 360 |
+
],
|
| 361 |
+
"baseline_keys": [],
|
| 362 |
+
"rule": "Files are scanned when opened. macOS: NotApplicable unless a third-party scanner provides it.",
|
| 363 |
+
"controls": {
|
| 364 |
+
"iso27001": [
|
| 365 |
+
"A.8.7"
|
| 366 |
+
],
|
| 367 |
+
"gdpr": [
|
| 368 |
+
"Art. 32(1)(b)"
|
| 369 |
+
],
|
| 370 |
+
"ai_act": []
|
| 371 |
+
}
|
| 372 |
+
},
|
| 373 |
+
{
|
| 374 |
+
"id": "malware.tamper",
|
| 375 |
+
"section": "baseline",
|
| 376 |
+
"name": "Tamper protection",
|
| 377 |
+
"os": [
|
| 378 |
+
"windows"
|
| 379 |
+
],
|
| 380 |
+
"baseline_keys": [],
|
| 381 |
+
"rule": "Defender tamper protection is on.",
|
| 382 |
+
"controls": {
|
| 383 |
+
"iso27001": [
|
| 384 |
+
"A.8.7"
|
| 385 |
+
],
|
| 386 |
+
"gdpr": [
|
| 387 |
+
"Art. 32(1)(b)"
|
| 388 |
+
],
|
| 389 |
+
"ai_act": []
|
| 390 |
+
}
|
| 391 |
+
},
|
| 392 |
+
{
|
| 393 |
+
"id": "malware.signatures",
|
| 394 |
+
"section": "baseline",
|
| 395 |
+
"name": "Malware signatures",
|
| 396 |
+
"names": {
|
| 397 |
+
"windows": "Security intelligence",
|
| 398 |
+
"macos": "Malware protection signatures"
|
| 399 |
+
},
|
| 400 |
+
"os": [
|
| 401 |
+
"windows",
|
| 402 |
+
"linux",
|
| 403 |
+
"macos"
|
| 404 |
+
],
|
| 405 |
+
"baseline_keys": [
|
| 406 |
+
"SIGNATURE_MAX_AGE_DAYS",
|
| 407 |
+
"MACOS_XPROTECT_MAX_AGE_DAYS"
|
| 408 |
+
],
|
| 409 |
+
"rule": "Signatures no older than SIGNATURE_MAX_AGE_DAYS (macOS XProtect: MACOS_XPROTECT_MAX_AGE_DAYS).",
|
| 410 |
+
"controls": {
|
| 411 |
+
"iso27001": [
|
| 412 |
+
"A.8.7",
|
| 413 |
+
"A.8.8"
|
| 414 |
+
],
|
| 415 |
+
"gdpr": [
|
| 416 |
+
"Art. 32(1)(b)"
|
| 417 |
+
],
|
| 418 |
+
"ai_act": []
|
| 419 |
+
}
|
| 420 |
+
},
|
| 421 |
+
{
|
| 422 |
+
"id": "malware.signature_updates",
|
| 423 |
+
"section": "baseline",
|
| 424 |
+
"name": "Malware signature updates",
|
| 425 |
+
"names": {
|
| 426 |
+
"macos": "Malware security updates"
|
| 427 |
+
},
|
| 428 |
+
"os": [
|
| 429 |
+
"linux",
|
| 430 |
+
"macos"
|
| 431 |
+
],
|
| 432 |
+
"baseline_keys": [],
|
| 433 |
+
"rule": "Automatic signature/security-data updates are enabled.",
|
| 434 |
+
"controls": {
|
| 435 |
+
"iso27001": [
|
| 436 |
+
"A.8.7",
|
| 437 |
+
"A.8.8"
|
| 438 |
+
],
|
| 439 |
+
"gdpr": [
|
| 440 |
+
"Art. 32(1)(b)"
|
| 441 |
+
],
|
| 442 |
+
"ai_act": []
|
| 443 |
+
}
|
| 444 |
+
},
|
| 445 |
+
{
|
| 446 |
+
"id": "firewall.enabled",
|
| 447 |
+
"section": "baseline",
|
| 448 |
+
"name": "Firewall",
|
| 449 |
+
"os": [
|
| 450 |
+
"linux",
|
| 451 |
+
"macos"
|
| 452 |
+
],
|
| 453 |
+
"baseline_keys": [],
|
| 454 |
+
"rule": "A host firewall is active.",
|
| 455 |
+
"controls": {
|
| 456 |
+
"iso27001": [
|
| 457 |
+
"A.8.20",
|
| 458 |
+
"A.8.1"
|
| 459 |
+
],
|
| 460 |
+
"gdpr": [
|
| 461 |
+
"Art. 32(1)(b)"
|
| 462 |
+
],
|
| 463 |
+
"ai_act": []
|
| 464 |
+
}
|
| 465 |
+
},
|
| 466 |
+
{
|
| 467 |
+
"id": "firewall.inbound",
|
| 468 |
+
"section": "baseline",
|
| 469 |
+
"name": "Firewall inbound policy",
|
| 470 |
+
"os": [
|
| 471 |
+
"linux",
|
| 472 |
+
"macos"
|
| 473 |
+
],
|
| 474 |
+
"baseline_keys": [],
|
| 475 |
+
"rule": "Unsolicited inbound traffic is blocked by default (macOS: firewall on with stealth mode).",
|
| 476 |
+
"controls": {
|
| 477 |
+
"iso27001": [
|
| 478 |
+
"A.8.20"
|
| 479 |
+
],
|
| 480 |
+
"gdpr": [
|
| 481 |
+
"Art. 32(1)(b)"
|
| 482 |
+
],
|
| 483 |
+
"ai_act": []
|
| 484 |
+
}
|
| 485 |
+
},
|
| 486 |
+
{
|
| 487 |
+
"id": "firewall.outbound",
|
| 488 |
+
"section": "baseline",
|
| 489 |
+
"name": "Firewall outbound policy",
|
| 490 |
+
"os": [
|
| 491 |
+
"linux",
|
| 492 |
+
"macos"
|
| 493 |
+
],
|
| 494 |
+
"baseline_keys": [
|
| 495 |
+
"FIREWALL_OUTBOUND"
|
| 496 |
+
],
|
| 497 |
+
"rule": "Default outbound policy equals FIREWALL_OUTBOUND.",
|
| 498 |
+
"controls": {
|
| 499 |
+
"iso27001": [
|
| 500 |
+
"A.8.20"
|
| 501 |
+
],
|
| 502 |
+
"gdpr": [
|
| 503 |
+
"Art. 32(1)(b)"
|
| 504 |
+
],
|
| 505 |
+
"ai_act": []
|
| 506 |
+
}
|
| 507 |
+
},
|
| 508 |
+
{
|
| 509 |
+
"id": "firewall.domain.enabled",
|
| 510 |
+
"section": "baseline",
|
| 511 |
+
"name": "Domain firewall",
|
| 512 |
+
"os": [
|
| 513 |
+
"windows"
|
| 514 |
+
],
|
| 515 |
+
"baseline_keys": [],
|
| 516 |
+
"rule": "Domain profile enabled.",
|
| 517 |
+
"controls": {
|
| 518 |
+
"iso27001": [
|
| 519 |
+
"A.8.20",
|
| 520 |
+
"A.8.1"
|
| 521 |
+
],
|
| 522 |
+
"gdpr": [
|
| 523 |
+
"Art. 32(1)(b)"
|
| 524 |
+
],
|
| 525 |
+
"ai_act": []
|
| 526 |
+
}
|
| 527 |
+
},
|
| 528 |
+
{
|
| 529 |
+
"id": "firewall.domain.inbound",
|
| 530 |
+
"section": "baseline",
|
| 531 |
+
"name": "Domain inbound",
|
| 532 |
+
"os": [
|
| 533 |
+
"windows"
|
| 534 |
+
],
|
| 535 |
+
"baseline_keys": [],
|
| 536 |
+
"rule": "Domain profile default inbound action is Block.",
|
| 537 |
+
"controls": {
|
| 538 |
+
"iso27001": [
|
| 539 |
+
"A.8.20"
|
| 540 |
+
],
|
| 541 |
+
"gdpr": [
|
| 542 |
+
"Art. 32(1)(b)"
|
| 543 |
+
],
|
| 544 |
+
"ai_act": []
|
| 545 |
+
}
|
| 546 |
+
},
|
| 547 |
+
{
|
| 548 |
+
"id": "firewall.domain.outbound",
|
| 549 |
+
"section": "baseline",
|
| 550 |
+
"name": "Domain outbound",
|
| 551 |
+
"os": [
|
| 552 |
+
"windows"
|
| 553 |
+
],
|
| 554 |
+
"baseline_keys": [
|
| 555 |
+
"FIREWALL_OUTBOUND"
|
| 556 |
+
],
|
| 557 |
+
"rule": "Domain profile default outbound action equals FIREWALL_OUTBOUND.",
|
| 558 |
+
"controls": {
|
| 559 |
+
"iso27001": [
|
| 560 |
+
"A.8.20"
|
| 561 |
+
],
|
| 562 |
+
"gdpr": [
|
| 563 |
+
"Art. 32(1)(b)"
|
| 564 |
+
],
|
| 565 |
+
"ai_act": []
|
| 566 |
+
}
|
| 567 |
+
},
|
| 568 |
+
{
|
| 569 |
+
"id": "firewall.private.enabled",
|
| 570 |
+
"section": "baseline",
|
| 571 |
+
"name": "Private firewall",
|
| 572 |
+
"os": [
|
| 573 |
+
"windows"
|
| 574 |
+
],
|
| 575 |
+
"baseline_keys": [],
|
| 576 |
+
"rule": "Private profile enabled.",
|
| 577 |
+
"controls": {
|
| 578 |
+
"iso27001": [
|
| 579 |
+
"A.8.20",
|
| 580 |
+
"A.8.1"
|
| 581 |
+
],
|
| 582 |
+
"gdpr": [
|
| 583 |
+
"Art. 32(1)(b)"
|
| 584 |
+
],
|
| 585 |
+
"ai_act": []
|
| 586 |
+
}
|
| 587 |
+
},
|
| 588 |
+
{
|
| 589 |
+
"id": "firewall.private.inbound",
|
| 590 |
+
"section": "baseline",
|
| 591 |
+
"name": "Private inbound",
|
| 592 |
+
"os": [
|
| 593 |
+
"windows"
|
| 594 |
+
],
|
| 595 |
+
"baseline_keys": [],
|
| 596 |
+
"rule": "Private profile default inbound action is Block.",
|
| 597 |
+
"controls": {
|
| 598 |
+
"iso27001": [
|
| 599 |
+
"A.8.20"
|
| 600 |
+
],
|
| 601 |
+
"gdpr": [
|
| 602 |
+
"Art. 32(1)(b)"
|
| 603 |
+
],
|
| 604 |
+
"ai_act": []
|
| 605 |
+
}
|
| 606 |
+
},
|
| 607 |
+
{
|
| 608 |
+
"id": "firewall.private.outbound",
|
| 609 |
+
"section": "baseline",
|
| 610 |
+
"name": "Private outbound",
|
| 611 |
+
"os": [
|
| 612 |
+
"windows"
|
| 613 |
+
],
|
| 614 |
+
"baseline_keys": [
|
| 615 |
+
"FIREWALL_OUTBOUND"
|
| 616 |
+
],
|
| 617 |
+
"rule": "Private profile default outbound action equals FIREWALL_OUTBOUND.",
|
| 618 |
+
"controls": {
|
| 619 |
+
"iso27001": [
|
| 620 |
+
"A.8.20"
|
| 621 |
+
],
|
| 622 |
+
"gdpr": [
|
| 623 |
+
"Art. 32(1)(b)"
|
| 624 |
+
],
|
| 625 |
+
"ai_act": []
|
| 626 |
+
}
|
| 627 |
+
},
|
| 628 |
+
{
|
| 629 |
+
"id": "firewall.public.enabled",
|
| 630 |
+
"section": "baseline",
|
| 631 |
+
"name": "Public firewall",
|
| 632 |
+
"os": [
|
| 633 |
+
"windows"
|
| 634 |
+
],
|
| 635 |
+
"baseline_keys": [],
|
| 636 |
+
"rule": "Public profile enabled.",
|
| 637 |
+
"controls": {
|
| 638 |
+
"iso27001": [
|
| 639 |
+
"A.8.20",
|
| 640 |
+
"A.8.1"
|
| 641 |
+
],
|
| 642 |
+
"gdpr": [
|
| 643 |
+
"Art. 32(1)(b)"
|
| 644 |
+
],
|
| 645 |
+
"ai_act": []
|
| 646 |
+
}
|
| 647 |
+
},
|
| 648 |
+
{
|
| 649 |
+
"id": "firewall.public.inbound",
|
| 650 |
+
"section": "baseline",
|
| 651 |
+
"name": "Public inbound",
|
| 652 |
+
"os": [
|
| 653 |
+
"windows"
|
| 654 |
+
],
|
| 655 |
+
"baseline_keys": [],
|
| 656 |
+
"rule": "Public profile default inbound action is Block.",
|
| 657 |
+
"controls": {
|
| 658 |
+
"iso27001": [
|
| 659 |
+
"A.8.20"
|
| 660 |
+
],
|
| 661 |
+
"gdpr": [
|
| 662 |
+
"Art. 32(1)(b)"
|
| 663 |
+
],
|
| 664 |
+
"ai_act": []
|
| 665 |
+
}
|
| 666 |
+
},
|
| 667 |
+
{
|
| 668 |
+
"id": "firewall.public.outbound",
|
| 669 |
+
"section": "baseline",
|
| 670 |
+
"name": "Public outbound",
|
| 671 |
+
"os": [
|
| 672 |
+
"windows"
|
| 673 |
+
],
|
| 674 |
+
"baseline_keys": [
|
| 675 |
+
"FIREWALL_OUTBOUND"
|
| 676 |
+
],
|
| 677 |
+
"rule": "Public profile default outbound action equals FIREWALL_OUTBOUND.",
|
| 678 |
+
"controls": {
|
| 679 |
+
"iso27001": [
|
| 680 |
+
"A.8.20"
|
| 681 |
+
],
|
| 682 |
+
"gdpr": [
|
| 683 |
+
"Art. 32(1)(b)"
|
| 684 |
+
],
|
| 685 |
+
"ai_act": []
|
| 686 |
+
}
|
| 687 |
+
},
|
| 688 |
+
{
|
| 689 |
+
"id": "screen_lock.enabled",
|
| 690 |
+
"section": "baseline",
|
| 691 |
+
"name": "Screen lock",
|
| 692 |
+
"os": [
|
| 693 |
+
"windows",
|
| 694 |
+
"linux",
|
| 695 |
+
"macos",
|
| 696 |
+
"ios",
|
| 697 |
+
"android"
|
| 698 |
+
],
|
| 699 |
+
"baseline_keys": [],
|
| 700 |
+
"rule": "The screen locks when idle (Windows: machine inactivity limit or a password-protected screen saver). Phones: a passcode / PIN / password screen lock is set.",
|
| 701 |
+
"controls": {
|
| 702 |
+
"iso27001": [
|
| 703 |
+
"A.8.1",
|
| 704 |
+
"A.7.7"
|
| 705 |
+
],
|
| 706 |
+
"gdpr": [
|
| 707 |
+
"Art. 32(1)(b)"
|
| 708 |
+
],
|
| 709 |
+
"ai_act": []
|
| 710 |
+
},
|
| 711 |
+
"names": {
|
| 712 |
+
"ios": "Passcode",
|
| 713 |
+
"android": "Screen lock"
|
| 714 |
+
}
|
| 715 |
+
},
|
| 716 |
+
{
|
| 717 |
+
"id": "screen_lock.timeout",
|
| 718 |
+
"section": "baseline",
|
| 719 |
+
"name": "Screen lock timeout",
|
| 720 |
+
"os": [
|
| 721 |
+
"windows",
|
| 722 |
+
"linux",
|
| 723 |
+
"macos",
|
| 724 |
+
"ios",
|
| 725 |
+
"android"
|
| 726 |
+
],
|
| 727 |
+
"baseline_keys": [
|
| 728 |
+
"SCREEN_LOCK_MAX_SECONDS"
|
| 729 |
+
],
|
| 730 |
+
"rule": "Idle time before lock (including any grace delay) is 1..SCREEN_LOCK_MAX_SECONDS seconds (baseline: 300 = 5 minutes). Phones: Auto-Lock / screen timeout.",
|
| 731 |
+
"controls": {
|
| 732 |
+
"iso27001": [
|
| 733 |
+
"A.8.1",
|
| 734 |
+
"A.7.7"
|
| 735 |
+
],
|
| 736 |
+
"gdpr": [
|
| 737 |
+
"Art. 32(1)(b)"
|
| 738 |
+
],
|
| 739 |
+
"ai_act": []
|
| 740 |
+
},
|
| 741 |
+
"names": {
|
| 742 |
+
"ios": "Auto-Lock",
|
| 743 |
+
"android": "Screen timeout"
|
| 744 |
+
}
|
| 745 |
+
},
|
| 746 |
+
{
|
| 747 |
+
"id": "screen_lock.password",
|
| 748 |
+
"section": "baseline",
|
| 749 |
+
"name": "Screen lock password requirement",
|
| 750 |
+
"os": [
|
| 751 |
+
"windows",
|
| 752 |
+
"linux",
|
| 753 |
+
"macos"
|
| 754 |
+
],
|
| 755 |
+
"baseline_keys": [],
|
| 756 |
+
"rule": "A password is required to unlock (macOS: within 5 seconds of the screen locking).",
|
| 757 |
+
"controls": {
|
| 758 |
+
"iso27001": [
|
| 759 |
+
"A.8.1",
|
| 760 |
+
"A.7.7",
|
| 761 |
+
"A.8.5"
|
| 762 |
+
],
|
| 763 |
+
"gdpr": [
|
| 764 |
+
"Art. 32(1)(b)"
|
| 765 |
+
],
|
| 766 |
+
"ai_act": []
|
| 767 |
+
}
|
| 768 |
+
},
|
| 769 |
+
{
|
| 770 |
+
"id": "mobile.passcode_strength",
|
| 771 |
+
"section": "baseline",
|
| 772 |
+
"name": "Passcode strength",
|
| 773 |
+
"os": [
|
| 774 |
+
"ios",
|
| 775 |
+
"android"
|
| 776 |
+
],
|
| 777 |
+
"names": {
|
| 778 |
+
"ios": "Passcode strength",
|
| 779 |
+
"android": "Screen lock strength"
|
| 780 |
+
},
|
| 781 |
+
"baseline_keys": [
|
| 782 |
+
"MOBILE_PASSCODE_MIN_DIGITS"
|
| 783 |
+
],
|
| 784 |
+
"rule": "Passcode / PIN has at least MOBILE_PASSCODE_MIN_DIGITS digits, or is alphanumeric. Android pattern or swipe locks do not comply.",
|
| 785 |
+
"controls": {
|
| 786 |
+
"iso27001": [
|
| 787 |
+
"A.5.17",
|
| 788 |
+
"A.8.5"
|
| 789 |
+
],
|
| 790 |
+
"gdpr": [
|
| 791 |
+
"Art. 32(1)(b)"
|
| 792 |
+
],
|
| 793 |
+
"ai_act": []
|
| 794 |
+
}
|
| 795 |
+
},
|
| 796 |
+
{
|
| 797 |
+
"id": "mobile.jailbreak_root",
|
| 798 |
+
"section": "baseline",
|
| 799 |
+
"name": "Operating system integrity",
|
| 800 |
+
"os": [
|
| 801 |
+
"ios",
|
| 802 |
+
"android"
|
| 803 |
+
],
|
| 804 |
+
"names": {
|
| 805 |
+
"ios": "Not jailbroken",
|
| 806 |
+
"android": "Not rooted"
|
| 807 |
+
},
|
| 808 |
+
"baseline_keys": [],
|
| 809 |
+
"rule": "The phone is not jailbroken (iOS) or rooted / bootloader-unlocked (Android).",
|
| 810 |
+
"controls": {
|
| 811 |
+
"iso27001": [
|
| 812 |
+
"A.8.1",
|
| 813 |
+
"A.8.7",
|
| 814 |
+
"A.8.19"
|
| 815 |
+
],
|
| 816 |
+
"gdpr": [
|
| 817 |
+
"Art. 32(1)(b)"
|
| 818 |
+
],
|
| 819 |
+
"ai_act": []
|
| 820 |
+
}
|
| 821 |
+
},
|
| 822 |
+
{
|
| 823 |
+
"id": "mobile.app_sources",
|
| 824 |
+
"section": "baseline",
|
| 825 |
+
"name": "App sources restricted",
|
| 826 |
+
"os": [
|
| 827 |
+
"ios",
|
| 828 |
+
"android"
|
| 829 |
+
],
|
| 830 |
+
"names": {
|
| 831 |
+
"ios": "Apps only from the App Store",
|
| 832 |
+
"android": "Install unknown apps blocked"
|
| 833 |
+
},
|
| 834 |
+
"baseline_keys": [],
|
| 835 |
+
"rule": "Apps are installed only from the official store (no alternative marketplaces, web distribution or unknown sources).",
|
| 836 |
+
"controls": {
|
| 837 |
+
"iso27001": [
|
| 838 |
+
"A.8.1",
|
| 839 |
+
"A.8.7",
|
| 840 |
+
"A.8.19"
|
| 841 |
+
],
|
| 842 |
+
"gdpr": [
|
| 843 |
+
"Art. 32(1)(b)"
|
| 844 |
+
],
|
| 845 |
+
"ai_act": []
|
| 846 |
+
}
|
| 847 |
+
},
|
| 848 |
+
{
|
| 849 |
+
"id": "mobile.usb_debugging",
|
| 850 |
+
"section": "baseline",
|
| 851 |
+
"name": "USB debugging off",
|
| 852 |
+
"os": [
|
| 853 |
+
"android"
|
| 854 |
+
],
|
| 855 |
+
"baseline_keys": [],
|
| 856 |
+
"rule": "Developer options / USB debugging are off.",
|
| 857 |
+
"controls": {
|
| 858 |
+
"iso27001": [
|
| 859 |
+
"A.8.1",
|
| 860 |
+
"A.8.19"
|
| 861 |
+
],
|
| 862 |
+
"gdpr": [
|
| 863 |
+
"Art. 32(1)(b)"
|
| 864 |
+
],
|
| 865 |
+
"ai_act": []
|
| 866 |
+
}
|
| 867 |
+
},
|
| 868 |
+
{
|
| 869 |
+
"id": "mobile.find_my_device",
|
| 870 |
+
"section": "baseline",
|
| 871 |
+
"name": "Find and erase a lost device",
|
| 872 |
+
"os": [
|
| 873 |
+
"ios",
|
| 874 |
+
"android"
|
| 875 |
+
],
|
| 876 |
+
"names": {
|
| 877 |
+
"ios": "Find My iPhone",
|
| 878 |
+
"android": "Find My Device"
|
| 879 |
+
},
|
| 880 |
+
"baseline_keys": [],
|
| 881 |
+
"rule": "Find My iPhone / Find My Device is on, so a lost phone can be located, locked and erased.",
|
| 882 |
+
"controls": {
|
| 883 |
+
"iso27001": [
|
| 884 |
+
"A.5.9",
|
| 885 |
+
"A.8.1"
|
| 886 |
+
],
|
| 887 |
+
"gdpr": [
|
| 888 |
+
"Art. 32(1)(b)",
|
| 889 |
+
"Art. 32(1)(c)"
|
| 890 |
+
],
|
| 891 |
+
"ai_act": []
|
| 892 |
+
}
|
| 893 |
+
},
|
| 894 |
+
{
|
| 895 |
+
"id": "usb.device_access",
|
| 896 |
+
"section": "baseline",
|
| 897 |
+
"name": "USB storage device access",
|
| 898 |
+
"os": [
|
| 899 |
+
"windows",
|
| 900 |
+
"linux",
|
| 901 |
+
"macos"
|
| 902 |
+
],
|
| 903 |
+
"baseline_keys": [
|
| 904 |
+
"USB_DEVICE_ACCESS"
|
| 905 |
+
],
|
| 906 |
+
"rule": "Whether USB mass storage devices can connect at all equals USB_DEVICE_ACCESS (allow/deny): driver, device-installation policy, USBGuard or MDM restriction.",
|
| 907 |
+
"controls": {
|
| 908 |
+
"iso27001": [
|
| 909 |
+
"A.7.10",
|
| 910 |
+
"A.8.12"
|
| 911 |
+
],
|
| 912 |
+
"gdpr": [
|
| 913 |
+
"Art. 32(1)(b)"
|
| 914 |
+
],
|
| 915 |
+
"ai_act": []
|
| 916 |
+
}
|
| 917 |
+
},
|
| 918 |
+
{
|
| 919 |
+
"id": "usb.read",
|
| 920 |
+
"section": "baseline",
|
| 921 |
+
"name": "USB storage read",
|
| 922 |
+
"os": [
|
| 923 |
+
"windows",
|
| 924 |
+
"linux",
|
| 925 |
+
"macos"
|
| 926 |
+
],
|
| 927 |
+
"baseline_keys": [
|
| 928 |
+
"USB_READ"
|
| 929 |
+
],
|
| 930 |
+
"rule": "Effective USB storage read access equals USB_READ (allow/deny).",
|
| 931 |
+
"controls": {
|
| 932 |
+
"iso27001": [
|
| 933 |
+
"A.7.10",
|
| 934 |
+
"A.8.12"
|
| 935 |
+
],
|
| 936 |
+
"gdpr": [
|
| 937 |
+
"Art. 32(1)(b)"
|
| 938 |
+
],
|
| 939 |
+
"ai_act": []
|
| 940 |
+
}
|
| 941 |
+
},
|
| 942 |
+
{
|
| 943 |
+
"id": "usb.write",
|
| 944 |
+
"section": "baseline",
|
| 945 |
+
"name": "USB storage write",
|
| 946 |
+
"os": [
|
| 947 |
+
"windows",
|
| 948 |
+
"linux",
|
| 949 |
+
"macos"
|
| 950 |
+
],
|
| 951 |
+
"baseline_keys": [
|
| 952 |
+
"USB_WRITE"
|
| 953 |
+
],
|
| 954 |
+
"rule": "Effective USB storage write access equals USB_WRITE (allow/deny).",
|
| 955 |
+
"controls": {
|
| 956 |
+
"iso27001": [
|
| 957 |
+
"A.7.10",
|
| 958 |
+
"A.8.12"
|
| 959 |
+
],
|
| 960 |
+
"gdpr": [
|
| 961 |
+
"Art. 32(1)(b)"
|
| 962 |
+
],
|
| 963 |
+
"ai_act": []
|
| 964 |
+
}
|
| 965 |
+
},
|
| 966 |
+
{
|
| 967 |
+
"id": "admin.approved",
|
| 968 |
+
"section": "baseline",
|
| 969 |
+
"name": "Approved IT administrator",
|
| 970 |
+
"os": [
|
| 971 |
+
"windows",
|
| 972 |
+
"linux",
|
| 973 |
+
"macos"
|
| 974 |
+
],
|
| 975 |
+
"baseline_keys": [
|
| 976 |
+
"APPROVED_ADMINS"
|
| 977 |
+
],
|
| 978 |
+
"rule": "At least one account listed in APPROVED_ADMINS is an enabled administrator.",
|
| 979 |
+
"controls": {
|
| 980 |
+
"iso27001": [
|
| 981 |
+
"A.8.2",
|
| 982 |
+
"A.5.15"
|
| 983 |
+
],
|
| 984 |
+
"gdpr": [
|
| 985 |
+
"Art. 32(1)(b)"
|
| 986 |
+
],
|
| 987 |
+
"ai_act": []
|
| 988 |
+
}
|
| 989 |
+
},
|
| 990 |
+
{
|
| 991 |
+
"id": "admin.unauthorized",
|
| 992 |
+
"section": "baseline",
|
| 993 |
+
"name": "Unauthorized employee administrator accounts",
|
| 994 |
+
"names": {
|
| 995 |
+
"windows": "Unauthorized employee local administrator accounts"
|
| 996 |
+
},
|
| 997 |
+
"os": [
|
| 998 |
+
"windows",
|
| 999 |
+
"linux",
|
| 1000 |
+
"macos"
|
| 1001 |
+
],
|
| 1002 |
+
"baseline_keys": [
|
| 1003 |
+
"APPROVED_ADMINS",
|
| 1004 |
+
"ALLOW_DISABLED_BUILTIN_ADMIN"
|
| 1005 |
+
],
|
| 1006 |
+
"rule": "No administrator accounts other than APPROVED_ADMINS (and the disabled built-in Administrator on Windows).",
|
| 1007 |
+
"controls": {
|
| 1008 |
+
"iso27001": [
|
| 1009 |
+
"A.8.2",
|
| 1010 |
+
"A.5.18"
|
| 1011 |
+
],
|
| 1012 |
+
"gdpr": [
|
| 1013 |
+
"Art. 32(1)(b)"
|
| 1014 |
+
],
|
| 1015 |
+
"ai_act": []
|
| 1016 |
+
}
|
| 1017 |
+
},
|
| 1018 |
+
{
|
| 1019 |
+
"id": "encryption.feature",
|
| 1020 |
+
"section": "baseline",
|
| 1021 |
+
"name": "BitLocker",
|
| 1022 |
+
"os": [
|
| 1023 |
+
"windows"
|
| 1024 |
+
],
|
| 1025 |
+
"baseline_keys": [],
|
| 1026 |
+
"rule": "BitLocker is turned on for the system drive.",
|
| 1027 |
+
"controls": {
|
| 1028 |
+
"iso27001": [
|
| 1029 |
+
"A.8.24",
|
| 1030 |
+
"A.8.1"
|
| 1031 |
+
],
|
| 1032 |
+
"gdpr": [
|
| 1033 |
+
"Art. 32(1)(a)"
|
| 1034 |
+
],
|
| 1035 |
+
"ai_act": []
|
| 1036 |
+
}
|
| 1037 |
+
},
|
| 1038 |
+
{
|
| 1039 |
+
"id": "encryption.enabled",
|
| 1040 |
+
"section": "baseline",
|
| 1041 |
+
"name": "Encryption",
|
| 1042 |
+
"os": [
|
| 1043 |
+
"windows",
|
| 1044 |
+
"linux",
|
| 1045 |
+
"macos",
|
| 1046 |
+
"ios",
|
| 1047 |
+
"android"
|
| 1048 |
+
],
|
| 1049 |
+
"baseline_keys": [],
|
| 1050 |
+
"rule": "The system disk is fully encrypted (BitLocker / LUKS / FileVault). Phones: iOS data protection is on when a passcode is set; Android 10+ encrypts by default.",
|
| 1051 |
+
"controls": {
|
| 1052 |
+
"iso27001": [
|
| 1053 |
+
"A.8.24",
|
| 1054 |
+
"A.8.1"
|
| 1055 |
+
],
|
| 1056 |
+
"gdpr": [
|
| 1057 |
+
"Art. 32(1)(a)"
|
| 1058 |
+
],
|
| 1059 |
+
"ai_act": []
|
| 1060 |
+
},
|
| 1061 |
+
"names": {
|
| 1062 |
+
"ios": "Data protection (encryption)",
|
| 1063 |
+
"android": "Device encryption"
|
| 1064 |
+
}
|
| 1065 |
+
},
|
| 1066 |
+
{
|
| 1067 |
+
"id": "encryption.protection",
|
| 1068 |
+
"section": "baseline",
|
| 1069 |
+
"name": "Disk encryption protection",
|
| 1070 |
+
"names": {
|
| 1071 |
+
"windows": "BitLocker protection"
|
| 1072 |
+
},
|
| 1073 |
+
"os": [
|
| 1074 |
+
"windows",
|
| 1075 |
+
"linux",
|
| 1076 |
+
"macos"
|
| 1077 |
+
],
|
| 1078 |
+
"baseline_keys": [],
|
| 1079 |
+
"rule": "Encryption protection is active (not suspended or in progress) with a strong cipher.",
|
| 1080 |
+
"controls": {
|
| 1081 |
+
"iso27001": [
|
| 1082 |
+
"A.8.24"
|
| 1083 |
+
],
|
| 1084 |
+
"gdpr": [
|
| 1085 |
+
"Art. 32(1)(a)"
|
| 1086 |
+
],
|
| 1087 |
+
"ai_act": []
|
| 1088 |
+
}
|
| 1089 |
+
},
|
| 1090 |
+
{
|
| 1091 |
+
"id": "encryption.recovery",
|
| 1092 |
+
"section": "baseline",
|
| 1093 |
+
"name": "Disk encryption recovery",
|
| 1094 |
+
"names": {
|
| 1095 |
+
"windows": "BitLocker recovery"
|
| 1096 |
+
},
|
| 1097 |
+
"os": [
|
| 1098 |
+
"windows",
|
| 1099 |
+
"linux",
|
| 1100 |
+
"macos"
|
| 1101 |
+
],
|
| 1102 |
+
"baseline_keys": [],
|
| 1103 |
+
"rule": "A recovery key or second unlock method exists.",
|
| 1104 |
+
"controls": {
|
| 1105 |
+
"iso27001": [
|
| 1106 |
+
"A.8.24",
|
| 1107 |
+
"A.8.13"
|
| 1108 |
+
],
|
| 1109 |
+
"gdpr": [
|
| 1110 |
+
"Art. 32(1)(a)",
|
| 1111 |
+
"Art. 32(1)(c)"
|
| 1112 |
+
],
|
| 1113 |
+
"ai_act": []
|
| 1114 |
+
}
|
| 1115 |
+
},
|
| 1116 |
+
{
|
| 1117 |
+
"id": "user.account_type",
|
| 1118 |
+
"section": "baseline",
|
| 1119 |
+
"name": "Signed-in user account type",
|
| 1120 |
+
"os": [
|
| 1121 |
+
"windows",
|
| 1122 |
+
"linux",
|
| 1123 |
+
"macos"
|
| 1124 |
+
],
|
| 1125 |
+
"baseline_keys": [
|
| 1126 |
+
"APPROVED_ADMINS"
|
| 1127 |
+
],
|
| 1128 |
+
"rule": "The everyday signed-in user is a Standard account (not an administrator), unless listed in APPROVED_ADMINS.",
|
| 1129 |
+
"controls": {
|
| 1130 |
+
"iso27001": [
|
| 1131 |
+
"A.8.2",
|
| 1132 |
+
"A.5.15"
|
| 1133 |
+
],
|
| 1134 |
+
"gdpr": [
|
| 1135 |
+
"Art. 32(1)(b)"
|
| 1136 |
+
],
|
| 1137 |
+
"ai_act": []
|
| 1138 |
+
}
|
| 1139 |
+
},
|
| 1140 |
+
{
|
| 1141 |
+
"id": "vuln.pending_updates",
|
| 1142 |
+
"section": "baseline",
|
| 1143 |
+
"name": "Pending security updates",
|
| 1144 |
+
"os": [
|
| 1145 |
+
"windows",
|
| 1146 |
+
"linux",
|
| 1147 |
+
"macos",
|
| 1148 |
+
"ios",
|
| 1149 |
+
"android"
|
| 1150 |
+
],
|
| 1151 |
+
"baseline_keys": [
|
| 1152 |
+
"MAX_PENDING_SECURITY_UPDATES",
|
| 1153 |
+
"MOBILE_PATCH_MAX_AGE_DAYS"
|
| 1154 |
+
],
|
| 1155 |
+
"rule": "Security updates offered by the OS vendor but not yet installed <= MAX_PENDING_SECURITY_UPDATES. Phones: iOS is at the latest security release known to the vulnerability data; Android security patch level is at most MOBILE_PATCH_MAX_AGE_DAYS old.",
|
| 1156 |
+
"controls": {
|
| 1157 |
+
"iso27001": [
|
| 1158 |
+
"A.8.8"
|
| 1159 |
+
],
|
| 1160 |
+
"gdpr": [
|
| 1161 |
+
"Art. 32(1)(b)",
|
| 1162 |
+
"Art. 32(1)(d)"
|
| 1163 |
+
],
|
| 1164 |
+
"ai_act": []
|
| 1165 |
+
},
|
| 1166 |
+
"names": {
|
| 1167 |
+
"ios": "iOS security updates",
|
| 1168 |
+
"android": "Android security update"
|
| 1169 |
+
}
|
| 1170 |
+
},
|
| 1171 |
+
{
|
| 1172 |
+
"id": "vuln.outdated_apps",
|
| 1173 |
+
"section": "baseline",
|
| 1174 |
+
"name": "Outdated third-party applications",
|
| 1175 |
+
"os": [
|
| 1176 |
+
"windows",
|
| 1177 |
+
"linux",
|
| 1178 |
+
"macos",
|
| 1179 |
+
"ios",
|
| 1180 |
+
"android"
|
| 1181 |
+
],
|
| 1182 |
+
"baseline_keys": [
|
| 1183 |
+
"MAX_OUTDATED_APPS"
|
| 1184 |
+
],
|
| 1185 |
+
"rule": "Installed third-party applications with a newer version available <= MAX_OUTDATED_APPS (Windows: winget; macOS: app versions; Linux: snap/flatpak and packages). Phones: automatic app updates are on.",
|
| 1186 |
+
"controls": {
|
| 1187 |
+
"iso27001": [
|
| 1188 |
+
"A.8.8",
|
| 1189 |
+
"A.8.19"
|
| 1190 |
+
],
|
| 1191 |
+
"gdpr": [
|
| 1192 |
+
"Art. 32(1)(b)"
|
| 1193 |
+
],
|
| 1194 |
+
"ai_act": []
|
| 1195 |
+
},
|
| 1196 |
+
"names": {
|
| 1197 |
+
"ios": "App updates",
|
| 1198 |
+
"android": "App updates"
|
| 1199 |
+
}
|
| 1200 |
+
},
|
| 1201 |
+
{
|
| 1202 |
+
"id": "vuln.known_vulnerabilities",
|
| 1203 |
+
"section": "baseline",
|
| 1204 |
+
"name": "Known vulnerabilities in installed software",
|
| 1205 |
+
"os": [
|
| 1206 |
+
"windows",
|
| 1207 |
+
"linux",
|
| 1208 |
+
"macos",
|
| 1209 |
+
"ios"
|
| 1210 |
+
],
|
| 1211 |
+
"baseline_keys": [
|
| 1212 |
+
"MAX_CRITICAL_HIGH_VULNERABILITIES"
|
| 1213 |
+
],
|
| 1214 |
+
"rule": "Installed software versions matched against an offline copy of the OSV/NVD vulnerability data: critical or high vulnerabilities <= MAX_CRITICAL_HIGH_VULNERABILITIES. iOS: the iOS version is matched against NVD iOS/iPadOS records.",
|
| 1215 |
+
"controls": {
|
| 1216 |
+
"iso27001": [
|
| 1217 |
+
"A.8.8"
|
| 1218 |
+
],
|
| 1219 |
+
"gdpr": [
|
| 1220 |
+
"Art. 32(1)(b)"
|
| 1221 |
+
],
|
| 1222 |
+
"ai_act": []
|
| 1223 |
+
},
|
| 1224 |
+
"names": {
|
| 1225 |
+
"ios": "Known vulnerabilities in iOS"
|
| 1226 |
+
}
|
| 1227 |
+
},
|
| 1228 |
+
{
|
| 1229 |
+
"id": "gdpr.auto_updates",
|
| 1230 |
+
"section": "gdpr",
|
| 1231 |
+
"name": "Automatic security updates",
|
| 1232 |
+
"os": [
|
| 1233 |
+
"windows",
|
| 1234 |
+
"linux",
|
| 1235 |
+
"macos",
|
| 1236 |
+
"ios",
|
| 1237 |
+
"android"
|
| 1238 |
+
],
|
| 1239 |
+
"baseline_keys": [],
|
| 1240 |
+
"rule": "Security updates are downloaded and installed automatically.",
|
| 1241 |
+
"controls": {
|
| 1242 |
+
"iso27001": [
|
| 1243 |
+
"A.8.8"
|
| 1244 |
+
],
|
| 1245 |
+
"gdpr": [
|
| 1246 |
+
"Art. 32(1)(b)"
|
| 1247 |
+
],
|
| 1248 |
+
"ai_act": []
|
| 1249 |
+
},
|
| 1250 |
+
"names": {
|
| 1251 |
+
"ios": "Automatic iOS updates",
|
| 1252 |
+
"android": "Automatic system updates"
|
| 1253 |
+
}
|
| 1254 |
+
},
|
| 1255 |
+
{
|
| 1256 |
+
"id": "gdpr.updates_recent",
|
| 1257 |
+
"section": "gdpr",
|
| 1258 |
+
"name": "Security updates installed recently",
|
| 1259 |
+
"os": [
|
| 1260 |
+
"windows",
|
| 1261 |
+
"linux",
|
| 1262 |
+
"macos"
|
| 1263 |
+
],
|
| 1264 |
+
"baseline_keys": [
|
| 1265 |
+
"UPDATES_MAX_AGE_DAYS"
|
| 1266 |
+
],
|
| 1267 |
+
"rule": "Last successful update/package change within UPDATES_MAX_AGE_DAYS days.",
|
| 1268 |
+
"controls": {
|
| 1269 |
+
"iso27001": [
|
| 1270 |
+
"A.8.8"
|
| 1271 |
+
],
|
| 1272 |
+
"gdpr": [
|
| 1273 |
+
"Art. 32(1)(b)",
|
| 1274 |
+
"Art. 32(1)(d)"
|
| 1275 |
+
],
|
| 1276 |
+
"ai_act": []
|
| 1277 |
+
}
|
| 1278 |
+
},
|
| 1279 |
+
{
|
| 1280 |
+
"id": "gdpr.os_supported",
|
| 1281 |
+
"section": "gdpr",
|
| 1282 |
+
"name": "Supported operating system version",
|
| 1283 |
+
"os": [
|
| 1284 |
+
"windows",
|
| 1285 |
+
"linux",
|
| 1286 |
+
"macos",
|
| 1287 |
+
"ios",
|
| 1288 |
+
"android"
|
| 1289 |
+
],
|
| 1290 |
+
"baseline_keys": [
|
| 1291 |
+
"WINDOWS_MIN_BUILD",
|
| 1292 |
+
"MACOS_MIN_MAJOR",
|
| 1293 |
+
"UBUNTU_MIN_VERSION",
|
| 1294 |
+
"DEBIAN_MIN_VERSION",
|
| 1295 |
+
"RHEL_MIN_VERSION",
|
| 1296 |
+
"IOS_MIN_MAJOR",
|
| 1297 |
+
"ANDROID_MIN_MAJOR"
|
| 1298 |
+
],
|
| 1299 |
+
"rule": "The OS version still receives security updates (minimums in the baseline). Phones: IOS_MIN_MAJOR / ANDROID_MIN_MAJOR.",
|
| 1300 |
+
"controls": {
|
| 1301 |
+
"iso27001": [
|
| 1302 |
+
"A.8.8"
|
| 1303 |
+
],
|
| 1304 |
+
"gdpr": [
|
| 1305 |
+
"Art. 32(1)(b)"
|
| 1306 |
+
],
|
| 1307 |
+
"ai_act": []
|
| 1308 |
+
}
|
| 1309 |
+
},
|
| 1310 |
+
{
|
| 1311 |
+
"id": "gdpr.security_logging",
|
| 1312 |
+
"section": "gdpr",
|
| 1313 |
+
"name": "Security event logging",
|
| 1314 |
+
"os": [
|
| 1315 |
+
"windows",
|
| 1316 |
+
"linux",
|
| 1317 |
+
"macos"
|
| 1318 |
+
],
|
| 1319 |
+
"baseline_keys": [],
|
| 1320 |
+
"rule": "Logon and account-change events are recorded (Windows audit policy, Linux auditd, macOS unified log).",
|
| 1321 |
+
"controls": {
|
| 1322 |
+
"iso27001": [
|
| 1323 |
+
"A.8.15"
|
| 1324 |
+
],
|
| 1325 |
+
"gdpr": [
|
| 1326 |
+
"Art. 32(1)(d)",
|
| 1327 |
+
"Art. 33"
|
| 1328 |
+
],
|
| 1329 |
+
"ai_act": []
|
| 1330 |
+
}
|
| 1331 |
+
},
|
| 1332 |
+
{
|
| 1333 |
+
"id": "gdpr.time_sync",
|
| 1334 |
+
"section": "gdpr",
|
| 1335 |
+
"name": "Time synchronisation",
|
| 1336 |
+
"os": [
|
| 1337 |
+
"windows",
|
| 1338 |
+
"linux",
|
| 1339 |
+
"macos",
|
| 1340 |
+
"ios",
|
| 1341 |
+
"android"
|
| 1342 |
+
],
|
| 1343 |
+
"baseline_keys": [],
|
| 1344 |
+
"rule": "The clock is synchronised from a time server.",
|
| 1345 |
+
"controls": {
|
| 1346 |
+
"iso27001": [
|
| 1347 |
+
"A.8.17"
|
| 1348 |
+
],
|
| 1349 |
+
"gdpr": [
|
| 1350 |
+
"Art. 33"
|
| 1351 |
+
],
|
| 1352 |
+
"ai_act": []
|
| 1353 |
+
},
|
| 1354 |
+
"names": {
|
| 1355 |
+
"ios": "Date & time set automatically",
|
| 1356 |
+
"android": "Date & time set automatically"
|
| 1357 |
+
}
|
| 1358 |
+
},
|
| 1359 |
+
{
|
| 1360 |
+
"id": "gdpr.backup_configured",
|
| 1361 |
+
"section": "gdpr",
|
| 1362 |
+
"name": "Backup configured",
|
| 1363 |
+
"os": [
|
| 1364 |
+
"windows",
|
| 1365 |
+
"linux",
|
| 1366 |
+
"macos",
|
| 1367 |
+
"ios",
|
| 1368 |
+
"android"
|
| 1369 |
+
],
|
| 1370 |
+
"baseline_keys": [],
|
| 1371 |
+
"rule": "A backup solution is configured for user data.",
|
| 1372 |
+
"controls": {
|
| 1373 |
+
"iso27001": [
|
| 1374 |
+
"A.8.13"
|
| 1375 |
+
],
|
| 1376 |
+
"gdpr": [
|
| 1377 |
+
"Art. 32(1)(c)"
|
| 1378 |
+
],
|
| 1379 |
+
"ai_act": []
|
| 1380 |
+
},
|
| 1381 |
+
"names": {
|
| 1382 |
+
"ios": "iCloud Backup",
|
| 1383 |
+
"android": "Google backup"
|
| 1384 |
+
}
|
| 1385 |
+
},
|
| 1386 |
+
{
|
| 1387 |
+
"id": "gdpr.backup_recent",
|
| 1388 |
+
"section": "gdpr",
|
| 1389 |
+
"name": "Recent backup",
|
| 1390 |
+
"os": [
|
| 1391 |
+
"windows",
|
| 1392 |
+
"linux",
|
| 1393 |
+
"macos"
|
| 1394 |
+
],
|
| 1395 |
+
"baseline_keys": [
|
| 1396 |
+
"BACKUP_MAX_AGE_DAYS"
|
| 1397 |
+
],
|
| 1398 |
+
"rule": "Last backup within BACKUP_MAX_AGE_DAYS days (Pending when the date cannot be read).",
|
| 1399 |
+
"controls": {
|
| 1400 |
+
"iso27001": [
|
| 1401 |
+
"A.8.13"
|
| 1402 |
+
],
|
| 1403 |
+
"gdpr": [
|
| 1404 |
+
"Art. 32(1)(c)"
|
| 1405 |
+
],
|
| 1406 |
+
"ai_act": []
|
| 1407 |
+
}
|
| 1408 |
+
},
|
| 1409 |
+
{
|
| 1410 |
+
"id": "gdpr.removable_encryption",
|
| 1411 |
+
"section": "gdpr",
|
| 1412 |
+
"name": "Removable media encryption",
|
| 1413 |
+
"os": [
|
| 1414 |
+
"windows",
|
| 1415 |
+
"linux",
|
| 1416 |
+
"macos"
|
| 1417 |
+
],
|
| 1418 |
+
"baseline_keys": [],
|
| 1419 |
+
"rule": "Data cannot be written to unencrypted removable drives.",
|
| 1420 |
+
"controls": {
|
| 1421 |
+
"iso27001": [
|
| 1422 |
+
"A.7.10",
|
| 1423 |
+
"A.8.24"
|
| 1424 |
+
],
|
| 1425 |
+
"gdpr": [
|
| 1426 |
+
"Art. 32(1)(a)",
|
| 1427 |
+
"Art. 34"
|
| 1428 |
+
],
|
| 1429 |
+
"ai_act": []
|
| 1430 |
+
}
|
| 1431 |
+
},
|
| 1432 |
+
{
|
| 1433 |
+
"id": "gdpr.guest_account",
|
| 1434 |
+
"section": "gdpr",
|
| 1435 |
+
"name": "Guest account disabled",
|
| 1436 |
+
"os": [
|
| 1437 |
+
"windows",
|
| 1438 |
+
"linux",
|
| 1439 |
+
"macos"
|
| 1440 |
+
],
|
| 1441 |
+
"baseline_keys": [],
|
| 1442 |
+
"rule": "No enabled guest login.",
|
| 1443 |
+
"controls": {
|
| 1444 |
+
"iso27001": [
|
| 1445 |
+
"A.5.16",
|
| 1446 |
+
"A.8.5"
|
| 1447 |
+
],
|
| 1448 |
+
"gdpr": [
|
| 1449 |
+
"Art. 32(1)(b)"
|
| 1450 |
+
],
|
| 1451 |
+
"ai_act": []
|
| 1452 |
+
}
|
| 1453 |
+
},
|
| 1454 |
+
{
|
| 1455 |
+
"id": "gdpr.remote_access",
|
| 1456 |
+
"section": "gdpr",
|
| 1457 |
+
"name": "Remote access services restricted",
|
| 1458 |
+
"os": [
|
| 1459 |
+
"windows",
|
| 1460 |
+
"linux",
|
| 1461 |
+
"macos"
|
| 1462 |
+
],
|
| 1463 |
+
"baseline_keys": [
|
| 1464 |
+
"APPROVED_REMOTE_SERVICES"
|
| 1465 |
+
],
|
| 1466 |
+
"rule": "Only remote access services listed in APPROVED_REMOTE_SERVICES are enabled.",
|
| 1467 |
+
"controls": {
|
| 1468 |
+
"iso27001": [
|
| 1469 |
+
"A.8.20",
|
| 1470 |
+
"A.6.7"
|
| 1471 |
+
],
|
| 1472 |
+
"gdpr": [
|
| 1473 |
+
"Art. 32(1)(b)"
|
| 1474 |
+
],
|
| 1475 |
+
"ai_act": []
|
| 1476 |
+
}
|
| 1477 |
+
},
|
| 1478 |
+
{
|
| 1479 |
+
"id": "gdpr.telemetry",
|
| 1480 |
+
"section": "gdpr",
|
| 1481 |
+
"name": "Diagnostic data minimised",
|
| 1482 |
+
"os": [
|
| 1483 |
+
"windows",
|
| 1484 |
+
"linux",
|
| 1485 |
+
"macos",
|
| 1486 |
+
"ios",
|
| 1487 |
+
"android"
|
| 1488 |
+
],
|
| 1489 |
+
"baseline_keys": [],
|
| 1490 |
+
"rule": "OS diagnostic/usage data sharing is at the minimum level.",
|
| 1491 |
+
"controls": {
|
| 1492 |
+
"iso27001": [
|
| 1493 |
+
"A.5.34"
|
| 1494 |
+
],
|
| 1495 |
+
"gdpr": [
|
| 1496 |
+
"Art. 5(1)(c)",
|
| 1497 |
+
"Art. 25"
|
| 1498 |
+
],
|
| 1499 |
+
"ai_act": []
|
| 1500 |
+
},
|
| 1501 |
+
"names": {
|
| 1502 |
+
"ios": "Analytics sharing minimised",
|
| 1503 |
+
"android": "Usage & diagnostics sharing minimised"
|
| 1504 |
+
}
|
| 1505 |
+
},
|
| 1506 |
+
{
|
| 1507 |
+
"id": "gdpr.lock_screen_previews",
|
| 1508 |
+
"section": "gdpr",
|
| 1509 |
+
"name": "Notification content hidden on the lock screen",
|
| 1510 |
+
"os": [
|
| 1511 |
+
"ios",
|
| 1512 |
+
"android"
|
| 1513 |
+
],
|
| 1514 |
+
"names": {
|
| 1515 |
+
"ios": "Notification previews hidden when locked",
|
| 1516 |
+
"android": "Sensitive notifications hidden when locked"
|
| 1517 |
+
},
|
| 1518 |
+
"baseline_keys": [],
|
| 1519 |
+
"rule": "Message and email previews are not shown on the locked screen.",
|
| 1520 |
+
"controls": {
|
| 1521 |
+
"iso27001": [
|
| 1522 |
+
"A.7.7",
|
| 1523 |
+
"A.8.1"
|
| 1524 |
+
],
|
| 1525 |
+
"gdpr": [
|
| 1526 |
+
"Art. 5(1)(f)",
|
| 1527 |
+
"Art. 32(1)(b)"
|
| 1528 |
+
],
|
| 1529 |
+
"ai_act": []
|
| 1530 |
+
}
|
| 1531 |
+
},
|
| 1532 |
+
{
|
| 1533 |
+
"id": "gdpr.personal_data_discovery",
|
| 1534 |
+
"section": "gdpr",
|
| 1535 |
+
"name": "Personal data discovery",
|
| 1536 |
+
"os": [
|
| 1537 |
+
"windows",
|
| 1538 |
+
"linux",
|
| 1539 |
+
"macos"
|
| 1540 |
+
],
|
| 1541 |
+
"baseline_keys": [
|
| 1542 |
+
"PERSONAL_DATA_SCAN"
|
| 1543 |
+
],
|
| 1544 |
+
"rule": "Opt-in scan of user folders for personal data patterns (counts and paths only). Not implemented in stage 1: NotApplicable.",
|
| 1545 |
+
"controls": {
|
| 1546 |
+
"iso27001": [
|
| 1547 |
+
"A.5.34",
|
| 1548 |
+
"A.5.12"
|
| 1549 |
+
],
|
| 1550 |
+
"gdpr": [
|
| 1551 |
+
"Art. 5(1)(c)",
|
| 1552 |
+
"Art. 5(1)(e)",
|
| 1553 |
+
"Art. 30"
|
| 1554 |
+
],
|
| 1555 |
+
"ai_act": []
|
| 1556 |
+
}
|
| 1557 |
+
},
|
| 1558 |
+
{
|
| 1559 |
+
"id": "aiact.inventory",
|
| 1560 |
+
"section": "aiact",
|
| 1561 |
+
"name": "AI software inventory",
|
| 1562 |
+
"os": [
|
| 1563 |
+
"windows",
|
| 1564 |
+
"linux",
|
| 1565 |
+
"macos",
|
| 1566 |
+
"ios",
|
| 1567 |
+
"android"
|
| 1568 |
+
],
|
| 1569 |
+
"baseline_keys": [],
|
| 1570 |
+
"rule": "AI apps, local AI services and local model folders are listed (Compliant = inventory taken).",
|
| 1571 |
+
"controls": {
|
| 1572 |
+
"iso27001": [
|
| 1573 |
+
"A.5.9"
|
| 1574 |
+
],
|
| 1575 |
+
"gdpr": [
|
| 1576 |
+
"Art. 30"
|
| 1577 |
+
],
|
| 1578 |
+
"ai_act": [
|
| 1579 |
+
"Art. 26"
|
| 1580 |
+
]
|
| 1581 |
+
}
|
| 1582 |
+
},
|
| 1583 |
+
{
|
| 1584 |
+
"id": "aiact.unapproved",
|
| 1585 |
+
"section": "aiact",
|
| 1586 |
+
"name": "Unapproved AI tools (shadow AI)",
|
| 1587 |
+
"os": [
|
| 1588 |
+
"windows",
|
| 1589 |
+
"linux",
|
| 1590 |
+
"macos",
|
| 1591 |
+
"ios",
|
| 1592 |
+
"android"
|
| 1593 |
+
],
|
| 1594 |
+
"baseline_keys": [
|
| 1595 |
+
"APPROVED_AI_TOOLS"
|
| 1596 |
+
],
|
| 1597 |
+
"rule": "Every AI tool found is listed in APPROVED_AI_TOOLS.",
|
| 1598 |
+
"controls": {
|
| 1599 |
+
"iso27001": [
|
| 1600 |
+
"A.5.9",
|
| 1601 |
+
"A.8.19"
|
| 1602 |
+
],
|
| 1603 |
+
"gdpr": [
|
| 1604 |
+
"Art. 28",
|
| 1605 |
+
"Art. 44"
|
| 1606 |
+
],
|
| 1607 |
+
"ai_act": [
|
| 1608 |
+
"Art. 26"
|
| 1609 |
+
]
|
| 1610 |
+
}
|
| 1611 |
+
},
|
| 1612 |
+
{
|
| 1613 |
+
"id": "aiact.risk_classification",
|
| 1614 |
+
"section": "aiact",
|
| 1615 |
+
"name": "AI system risk classification",
|
| 1616 |
+
"os": [
|
| 1617 |
+
"windows",
|
| 1618 |
+
"linux",
|
| 1619 |
+
"macos",
|
| 1620 |
+
"ios",
|
| 1621 |
+
"android"
|
| 1622 |
+
],
|
| 1623 |
+
"baseline_keys": [],
|
| 1624 |
+
"rule": "Each AI tool found is classified (prohibited / high-risk / limited / minimal). Done by the auditor model: Pending in the collector output, NotApplicable when no AI tool is found.",
|
| 1625 |
+
"controls": {
|
| 1626 |
+
"iso27001": [
|
| 1627 |
+
"A.5.9"
|
| 1628 |
+
],
|
| 1629 |
+
"gdpr": [],
|
| 1630 |
+
"ai_act": [
|
| 1631 |
+
"Art. 5",
|
| 1632 |
+
"Art. 6",
|
| 1633 |
+
"Annex III"
|
| 1634 |
+
]
|
| 1635 |
+
}
|
| 1636 |
+
},
|
| 1637 |
+
{
|
| 1638 |
+
"id": "aiact.os_ai_features",
|
| 1639 |
+
"section": "aiact",
|
| 1640 |
+
"name": "Operating system AI features restricted",
|
| 1641 |
+
"names": {
|
| 1642 |
+
"windows": "Windows Recall / Copilot data analysis disabled",
|
| 1643 |
+
"macos": "Apple Intelligence external integrations restricted",
|
| 1644 |
+
"ios": "Apple Intelligence external integrations restricted",
|
| 1645 |
+
"android": "AI assistant access restricted"
|
| 1646 |
+
},
|
| 1647 |
+
"os": [
|
| 1648 |
+
"windows",
|
| 1649 |
+
"linux",
|
| 1650 |
+
"macos",
|
| 1651 |
+
"ios",
|
| 1652 |
+
"android"
|
| 1653 |
+
],
|
| 1654 |
+
"baseline_keys": [],
|
| 1655 |
+
"rule": "OS-level AI features that capture or send user data are disabled by policy. NotApplicable where the OS has none. Phones: external AI integrations (ChatGPT in Apple Intelligence, assistant access to screen content) are off unless approved.",
|
| 1656 |
+
"controls": {
|
| 1657 |
+
"iso27001": [
|
| 1658 |
+
"A.5.34"
|
| 1659 |
+
],
|
| 1660 |
+
"gdpr": [
|
| 1661 |
+
"Art. 5(1)(c)",
|
| 1662 |
+
"Art. 25"
|
| 1663 |
+
],
|
| 1664 |
+
"ai_act": [
|
| 1665 |
+
"Art. 26"
|
| 1666 |
+
]
|
| 1667 |
+
}
|
| 1668 |
+
},
|
| 1669 |
+
{
|
| 1670 |
+
"id": "aiact.log_retention",
|
| 1671 |
+
"section": "aiact",
|
| 1672 |
+
"name": "AI usage log retention",
|
| 1673 |
+
"os": [
|
| 1674 |
+
"windows",
|
| 1675 |
+
"linux",
|
| 1676 |
+
"macos",
|
| 1677 |
+
"ios",
|
| 1678 |
+
"android"
|
| 1679 |
+
],
|
| 1680 |
+
"baseline_keys": [],
|
| 1681 |
+
"rule": "Logs kept >= 6 months if a high-risk AI system is used. Pending until classification; NotApplicable when no AI tool is found.",
|
| 1682 |
+
"controls": {
|
| 1683 |
+
"iso27001": [
|
| 1684 |
+
"A.8.15"
|
| 1685 |
+
],
|
| 1686 |
+
"gdpr": [],
|
| 1687 |
+
"ai_act": [
|
| 1688 |
+
"Art. 26(6)"
|
| 1689 |
+
]
|
| 1690 |
+
}
|
| 1691 |
+
},
|
| 1692 |
+
{
|
| 1693 |
+
"id": "org.ropa",
|
| 1694 |
+
"section": "org",
|
| 1695 |
+
"name": "Record of processing activities",
|
| 1696 |
+
"os": [
|
| 1697 |
+
"windows",
|
| 1698 |
+
"linux",
|
| 1699 |
+
"macos",
|
| 1700 |
+
"ios",
|
| 1701 |
+
"android"
|
| 1702 |
+
],
|
| 1703 |
+
"answer_key": "ORG_ROPA",
|
| 1704 |
+
"question": "Is there a current record of processing activities (RoPA)?",
|
| 1705 |
+
"baseline_keys": [],
|
| 1706 |
+
"rule": "Answer yes = Compliant, no/partial = Non-Compliant, n/a = NotApplicable, unanswered = Pending.",
|
| 1707 |
+
"controls": {
|
| 1708 |
+
"iso27001": [
|
| 1709 |
+
"A.5.34"
|
| 1710 |
+
],
|
| 1711 |
+
"gdpr": [
|
| 1712 |
+
"Art. 30"
|
| 1713 |
+
],
|
| 1714 |
+
"ai_act": []
|
| 1715 |
+
}
|
| 1716 |
+
},
|
| 1717 |
+
{
|
| 1718 |
+
"id": "org.dpia",
|
| 1719 |
+
"section": "org",
|
| 1720 |
+
"name": "Data protection impact assessment",
|
| 1721 |
+
"os": [
|
| 1722 |
+
"windows",
|
| 1723 |
+
"linux",
|
| 1724 |
+
"macos",
|
| 1725 |
+
"ios",
|
| 1726 |
+
"android"
|
| 1727 |
+
],
|
| 1728 |
+
"answer_key": "ORG_DPIA",
|
| 1729 |
+
"question": "Has a DPIA been done for every high-risk processing activity?",
|
| 1730 |
+
"baseline_keys": [],
|
| 1731 |
+
"rule": "As above.",
|
| 1732 |
+
"controls": {
|
| 1733 |
+
"iso27001": [
|
| 1734 |
+
"A.5.34"
|
| 1735 |
+
],
|
| 1736 |
+
"gdpr": [
|
| 1737 |
+
"Art. 35"
|
| 1738 |
+
],
|
| 1739 |
+
"ai_act": []
|
| 1740 |
+
}
|
| 1741 |
+
},
|
| 1742 |
+
{
|
| 1743 |
+
"id": "org.breach_procedure",
|
| 1744 |
+
"section": "org",
|
| 1745 |
+
"name": "Breach response procedure",
|
| 1746 |
+
"os": [
|
| 1747 |
+
"windows",
|
| 1748 |
+
"linux",
|
| 1749 |
+
"macos",
|
| 1750 |
+
"ios",
|
| 1751 |
+
"android"
|
| 1752 |
+
],
|
| 1753 |
+
"answer_key": "ORG_BREACH_PROCEDURE",
|
| 1754 |
+
"question": "Is there a tested procedure to report a personal data breach within 72 hours?",
|
| 1755 |
+
"baseline_keys": [],
|
| 1756 |
+
"rule": "As above.",
|
| 1757 |
+
"controls": {
|
| 1758 |
+
"iso27001": [
|
| 1759 |
+
"A.5.24",
|
| 1760 |
+
"A.5.26"
|
| 1761 |
+
],
|
| 1762 |
+
"gdpr": [
|
| 1763 |
+
"Art. 33",
|
| 1764 |
+
"Art. 34"
|
| 1765 |
+
],
|
| 1766 |
+
"ai_act": []
|
| 1767 |
+
}
|
| 1768 |
+
},
|
| 1769 |
+
{
|
| 1770 |
+
"id": "org.processor_agreements",
|
| 1771 |
+
"section": "org",
|
| 1772 |
+
"name": "Data processing agreements",
|
| 1773 |
+
"os": [
|
| 1774 |
+
"windows",
|
| 1775 |
+
"linux",
|
| 1776 |
+
"macos",
|
| 1777 |
+
"ios",
|
| 1778 |
+
"android"
|
| 1779 |
+
],
|
| 1780 |
+
"answer_key": "ORG_PROCESSOR_AGREEMENTS",
|
| 1781 |
+
"question": "Are data processing agreements signed with all processors, including AI providers?",
|
| 1782 |
+
"baseline_keys": [],
|
| 1783 |
+
"rule": "As above.",
|
| 1784 |
+
"controls": {
|
| 1785 |
+
"iso27001": [
|
| 1786 |
+
"A.5.20"
|
| 1787 |
+
],
|
| 1788 |
+
"gdpr": [
|
| 1789 |
+
"Art. 28"
|
| 1790 |
+
],
|
| 1791 |
+
"ai_act": []
|
| 1792 |
+
}
|
| 1793 |
+
},
|
| 1794 |
+
{
|
| 1795 |
+
"id": "org.ai_register",
|
| 1796 |
+
"section": "org",
|
| 1797 |
+
"name": "AI register",
|
| 1798 |
+
"os": [
|
| 1799 |
+
"windows",
|
| 1800 |
+
"linux",
|
| 1801 |
+
"macos",
|
| 1802 |
+
"ios",
|
| 1803 |
+
"android"
|
| 1804 |
+
],
|
| 1805 |
+
"answer_key": "ORG_AI_REGISTER",
|
| 1806 |
+
"question": "Is there a register of the AI systems the organisation uses?",
|
| 1807 |
+
"baseline_keys": [],
|
| 1808 |
+
"rule": "As above.",
|
| 1809 |
+
"controls": {
|
| 1810 |
+
"iso27001": [
|
| 1811 |
+
"A.5.9"
|
| 1812 |
+
],
|
| 1813 |
+
"gdpr": [],
|
| 1814 |
+
"ai_act": [
|
| 1815 |
+
"Art. 26"
|
| 1816 |
+
]
|
| 1817 |
+
}
|
| 1818 |
+
},
|
| 1819 |
+
{
|
| 1820 |
+
"id": "org.ai_literacy",
|
| 1821 |
+
"section": "org",
|
| 1822 |
+
"name": "AI literacy training",
|
| 1823 |
+
"os": [
|
| 1824 |
+
"windows",
|
| 1825 |
+
"linux",
|
| 1826 |
+
"macos",
|
| 1827 |
+
"ios",
|
| 1828 |
+
"android"
|
| 1829 |
+
],
|
| 1830 |
+
"answer_key": "ORG_AI_LITERACY",
|
| 1831 |
+
"question": "Have staff who use AI received AI literacy training?",
|
| 1832 |
+
"baseline_keys": [],
|
| 1833 |
+
"rule": "As above.",
|
| 1834 |
+
"controls": {
|
| 1835 |
+
"iso27001": [
|
| 1836 |
+
"A.6.3"
|
| 1837 |
+
],
|
| 1838 |
+
"gdpr": [],
|
| 1839 |
+
"ai_act": [
|
| 1840 |
+
"Art. 4"
|
| 1841 |
+
]
|
| 1842 |
+
}
|
| 1843 |
+
},
|
| 1844 |
+
{
|
| 1845 |
+
"id": "org.human_oversight",
|
| 1846 |
+
"section": "org",
|
| 1847 |
+
"name": "Human oversight for high-risk AI",
|
| 1848 |
+
"os": [
|
| 1849 |
+
"windows",
|
| 1850 |
+
"linux",
|
| 1851 |
+
"macos",
|
| 1852 |
+
"ios",
|
| 1853 |
+
"android"
|
| 1854 |
+
],
|
| 1855 |
+
"answer_key": "ORG_HUMAN_OVERSIGHT",
|
| 1856 |
+
"question": "Is a trained person assigned to oversee each high-risk AI system?",
|
| 1857 |
+
"baseline_keys": [],
|
| 1858 |
+
"rule": "As above.",
|
| 1859 |
+
"controls": {
|
| 1860 |
+
"iso27001": [],
|
| 1861 |
+
"gdpr": [],
|
| 1862 |
+
"ai_act": [
|
| 1863 |
+
"Art. 14",
|
| 1864 |
+
"Art. 26"
|
| 1865 |
+
]
|
| 1866 |
+
}
|
| 1867 |
+
},
|
| 1868 |
+
{
|
| 1869 |
+
"id": "org.ai_transparency",
|
| 1870 |
+
"section": "org",
|
| 1871 |
+
"name": "AI transparency to users",
|
| 1872 |
+
"os": [
|
| 1873 |
+
"windows",
|
| 1874 |
+
"linux",
|
| 1875 |
+
"macos",
|
| 1876 |
+
"ios",
|
| 1877 |
+
"android"
|
| 1878 |
+
],
|
| 1879 |
+
"answer_key": "ORG_AI_TRANSPARENCY",
|
| 1880 |
+
"question": "Are people told when they interact with AI or receive AI-generated content?",
|
| 1881 |
+
"baseline_keys": [],
|
| 1882 |
+
"rule": "As above.",
|
| 1883 |
+
"controls": {
|
| 1884 |
+
"iso27001": [],
|
| 1885 |
+
"gdpr": [],
|
| 1886 |
+
"ai_act": [
|
| 1887 |
+
"Art. 50"
|
| 1888 |
+
]
|
| 1889 |
+
}
|
| 1890 |
+
}
|
| 1891 |
]
|
| 1892 |
}
|
catalog/mobile_questions.json
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_comment": "Phone self-check: what the browser detects and what the user is asked, per check and OS. Read by mobile/mobile-core.js (web page) and, through it, by the synthetic generator, so training data uses exactly this wording. Answer fields: label (shown), actual (written to the evidence), status, detail (optional). Computed answers: seconds (timeout vs SCREEN_LOCK_MAX_SECONDS), digits/alphanumeric/pattern (passcode vs MOBILE_PASSCODE_MIN_DIGITS). Settings paths differ slightly between phone makers; the 'where' text says so.",
|
| 3 |
+
"version": "0.3.0",
|
| 4 |
+
"not_sure": {"label": "I'm not sure / I can't find it", "actual": "not answered", "status": "Pending", "detail": "The user could not find or confirm this setting."},
|
| 5 |
+
"os_version": {
|
| 6 |
+
"ios": {"question": "Which iOS version does this iPhone have?", "where": "Settings › General › About › iOS Version"},
|
| 7 |
+
"android": {"question": "Which Android version does this phone have?", "where": "Settings › About phone › Android version (sometimes under Software information)"}
|
| 8 |
+
},
|
| 9 |
+
"checks": {
|
| 10 |
+
"screen_lock.enabled": {
|
| 11 |
+
"ios": {"question": "Is a passcode set on this iPhone?", "where": "Settings › Face ID & Passcode (or Touch ID & Passcode). If it asks for your passcode before opening, one is set.", "expected": "passcode set",
|
| 12 |
+
"answers": [
|
| 13 |
+
{"label": "Yes, a passcode is set", "actual": "passcode set", "status": "Compliant"},
|
| 14 |
+
{"label": "No passcode", "actual": "no passcode", "status": "Non-Compliant", "detail": "Anyone holding the phone can open it."}]},
|
| 15 |
+
"android": {"question": "Which screen lock is set?", "where": "Settings › Security & privacy › Device unlock › Screen lock (names vary by phone maker)", "expected": "PIN, password or pattern",
|
| 16 |
+
"answers": [
|
| 17 |
+
{"label": "PIN, password or pattern", "actual": "screen lock set", "status": "Compliant"},
|
| 18 |
+
{"label": "Swipe or None", "actual": "no screen lock (Swipe/None)", "status": "Non-Compliant", "detail": "Anyone holding the phone can open it."}]}
|
| 19 |
+
},
|
| 20 |
+
"screen_lock.timeout": {
|
| 21 |
+
"ios": {"question": "What is Auto-Lock set to?", "where": "Settings › Display & Brightness › Auto-Lock", "expected_from": "seconds",
|
| 22 |
+
"answers": [
|
| 23 |
+
{"label": "30 seconds", "seconds": 30}, {"label": "1 minute", "seconds": 60}, {"label": "2 minutes", "seconds": 120},
|
| 24 |
+
{"label": "3 minutes", "seconds": 180}, {"label": "4 minutes", "seconds": 240}, {"label": "5 minutes", "seconds": 300},
|
| 25 |
+
{"label": "Never", "seconds": null}]},
|
| 26 |
+
"android": {"question": "What is Screen timeout set to?", "where": "Settings › Display › Screen timeout. If there is also 'Lock after screen timeout', it should be Immediately.", "expected_from": "seconds",
|
| 27 |
+
"answers": [
|
| 28 |
+
{"label": "15 seconds", "seconds": 15}, {"label": "30 seconds", "seconds": 30}, {"label": "1 minute", "seconds": 60},
|
| 29 |
+
{"label": "2 minutes", "seconds": 120}, {"label": "5 minutes", "seconds": 300}, {"label": "10 minutes", "seconds": 600},
|
| 30 |
+
{"label": "30 minutes", "seconds": 1800}]}
|
| 31 |
+
},
|
| 32 |
+
"mobile.passcode_strength": {
|
| 33 |
+
"ios": {"question": "What kind of passcode do you use?", "where": "The code you type to unlock. Settings › Face ID & Passcode › Change Passcode › Passcode Options shows the choices.", "expected_from": "passcode",
|
| 34 |
+
"answers": [
|
| 35 |
+
{"label": "4-digit code", "digits": 4}, {"label": "6-digit code", "digits": 6},
|
| 36 |
+
{"label": "Custom numeric code (7 or more digits)", "digits": 8}, {"label": "Custom alphanumeric code (letters and numbers)", "alphanumeric": true}]},
|
| 37 |
+
"android": {"question": "What kind of screen lock do you use?", "where": "Settings › Security & privacy › Device unlock › Screen lock", "expected_from": "passcode",
|
| 38 |
+
"answers": [
|
| 39 |
+
{"label": "Pattern", "pattern": true}, {"label": "4-digit PIN", "digits": 4}, {"label": "PIN of 6 or more digits", "digits": 6},
|
| 40 |
+
{"label": "Password (letters and numbers)", "alphanumeric": true}, {"label": "Swipe or None", "none": true}]}
|
| 41 |
+
},
|
| 42 |
+
"mobile.jailbreak_root": {
|
| 43 |
+
"ios": {"question": "Has this iPhone been jailbroken?", "where": "Jailbreaking means deliberately modifying iOS to install apps outside Apple's control. If nobody did this on purpose, answer No.", "expected": "not jailbroken",
|
| 44 |
+
"answers": [
|
| 45 |
+
{"label": "No", "actual": "not jailbroken (user confirmed)", "status": "Compliant"},
|
| 46 |
+
{"label": "Yes", "actual": "jailbroken", "status": "Non-Compliant", "detail": "iOS security protections are bypassed on a jailbroken phone."}]},
|
| 47 |
+
"android": {"question": "Is this phone rooted or is its bootloader unlocked?", "where": "Rooting means deliberately giving apps full system access. If nobody did this on purpose, answer No.", "expected": "not rooted",
|
| 48 |
+
"answers": [
|
| 49 |
+
{"label": "No", "actual": "not rooted (user confirmed)", "status": "Compliant"},
|
| 50 |
+
{"label": "Yes", "actual": "rooted or bootloader unlocked", "status": "Non-Compliant", "detail": "Android security protections are weakened on a rooted phone."}]}
|
| 51 |
+
},
|
| 52 |
+
"mobile.app_sources": {
|
| 53 |
+
"ios": {"question": "Where do the apps on this iPhone come from?", "where": "Settings › General › VPN & Device Management lists developer or enterprise profiles. Alternative app marketplaces appear as separate apps.", "expected": "App Store only",
|
| 54 |
+
"answers": [
|
| 55 |
+
{"label": "Only the App Store", "actual": "App Store only", "status": "Compliant"},
|
| 56 |
+
{"label": "App Store and apps installed by my employer", "actual": "App Store and employer-managed apps", "status": "Compliant"},
|
| 57 |
+
{"label": "Also another marketplace, a website or a developer profile", "actual": "apps from outside the App Store", "status": "Non-Compliant", "detail": "Apps from outside the App Store skip Apple's review."}]},
|
| 58 |
+
"android": {"question": "Is any app allowed to install unknown apps?", "where": "Settings › Apps › Special app access › Install unknown apps. Every app should show 'Not allowed'.", "expected": "all apps: Not allowed",
|
| 59 |
+
"answers": [
|
| 60 |
+
{"label": "All show 'Not allowed'", "actual": "unknown apps blocked for all apps", "status": "Compliant"},
|
| 61 |
+
{"label": "At least one shows 'Allowed'", "actual": "unknown apps allowed for at least one app", "status": "Non-Compliant", "detail": "Apps from outside Google Play skip Play Store checks."}]}
|
| 62 |
+
},
|
| 63 |
+
"mobile.usb_debugging": {
|
| 64 |
+
"android": {"question": "Is USB debugging on?", "where": "Settings › System › Developer options › USB debugging. If you cannot find Developer options, they are off.", "expected": "off",
|
| 65 |
+
"answers": [
|
| 66 |
+
{"label": "Off, or no Developer options", "actual": "off", "status": "Compliant"},
|
| 67 |
+
{"label": "On", "actual": "on", "status": "Non-Compliant", "detail": "A connected computer can read and change data on the phone."}]}
|
| 68 |
+
},
|
| 69 |
+
"mobile.find_my_device": {
|
| 70 |
+
"ios": {"question": "Is Find My iPhone on?", "where": "Settings › [your name] › Find My › Find My iPhone", "expected": "on",
|
| 71 |
+
"answers": [
|
| 72 |
+
{"label": "On", "actual": "on", "status": "Compliant"},
|
| 73 |
+
{"label": "Off", "actual": "off", "status": "Non-Compliant", "detail": "A lost phone cannot be located, locked or erased remotely."}]},
|
| 74 |
+
"android": {"question": "Is Find My Device (Find Hub) on?", "where": "Settings › Google › All services › Find My Device (called Find Hub on newer phones)", "expected": "on",
|
| 75 |
+
"answers": [
|
| 76 |
+
{"label": "On", "actual": "on", "status": "Compliant"},
|
| 77 |
+
{"label": "Off", "actual": "off", "status": "Non-Compliant", "detail": "A lost phone cannot be located, locked or erased remotely."}]}
|
| 78 |
+
},
|
| 79 |
+
"malware.antivirus": {
|
| 80 |
+
"android": {"question": "Is 'Scan apps with Play Protect' on?", "where": "Play Store › your profile picture › Play Protect › Settings (gear icon)", "expected": "on",
|
| 81 |
+
"answers": [
|
| 82 |
+
{"label": "On", "actual": "Play Protect scanning on", "status": "Compliant"},
|
| 83 |
+
{"label": "Off", "actual": "Play Protect scanning off", "status": "Non-Compliant", "detail": "Installed apps are not checked for harmful behaviour."}]}
|
| 84 |
+
},
|
| 85 |
+
"vuln.pending_updates": {
|
| 86 |
+
"android": {"question": "What is the Android security update date?", "where": "Settings › About phone › Android version › Android security update (or Settings › Security & privacy › Updates)", "expected_from": "patch_month", "input": "month"}
|
| 87 |
+
},
|
| 88 |
+
"vuln.outdated_apps": {
|
| 89 |
+
"ios": {"question": "Are automatic App Updates on?", "where": "Settings › App Store › App Updates", "expected": "automatic app updates on",
|
| 90 |
+
"answers": [
|
| 91 |
+
{"label": "On", "actual": "automatic app updates on", "status": "Compliant"},
|
| 92 |
+
{"label": "Off", "actual": "automatic app updates off", "status": "Non-Compliant", "detail": "Apps keep known security bugs until they are updated by hand."}]},
|
| 93 |
+
"android": {"question": "Are apps updated automatically?", "where": "Play Store › your profile picture › Settings › Network preferences › Auto-update apps", "expected": "automatic app updates on",
|
| 94 |
+
"answers": [
|
| 95 |
+
{"label": "Over any network, or over Wi-Fi only", "actual": "automatic app updates on", "status": "Compliant"},
|
| 96 |
+
{"label": "Don't auto-update apps", "actual": "automatic app updates off", "status": "Non-Compliant", "detail": "Apps keep known security bugs until they are updated by hand."}]}
|
| 97 |
+
},
|
| 98 |
+
"gdpr.auto_updates": {
|
| 99 |
+
"ios": {"question": "What do Automatic Updates show?", "where": "Settings › General › Software Update › Automatic Updates", "expected": "download and install on",
|
| 100 |
+
"answers": [
|
| 101 |
+
{"label": "Download and Install are both on", "actual": "download and install on", "status": "Compliant"},
|
| 102 |
+
{"label": "Only Download is on", "actual": "download only", "status": "Non-Compliant", "detail": "Updates wait for the user to install them."},
|
| 103 |
+
{"label": "Both off", "actual": "off", "status": "Non-Compliant", "detail": "Security updates are not installed automatically."}]},
|
| 104 |
+
"android": {"question": "Are system updates downloaded automatically?", "where": "Settings › System › Software updates (names vary: 'Auto download over Wi-Fi', 'Automatic system updates')", "expected": "automatic",
|
| 105 |
+
"answers": [
|
| 106 |
+
{"label": "Yes, automatically", "actual": "automatic", "status": "Compliant"},
|
| 107 |
+
{"label": "No, I install them by hand", "actual": "manual", "status": "Non-Compliant", "detail": "Security updates wait for the user."}]}
|
| 108 |
+
},
|
| 109 |
+
"gdpr.time_sync": {
|
| 110 |
+
"ios": {"question": "Is 'Set Automatically' on?", "where": "Settings › General › Date & Time", "expected": "set automatically",
|
| 111 |
+
"answers": [
|
| 112 |
+
{"label": "On", "actual": "set automatically", "status": "Compliant"},
|
| 113 |
+
{"label": "Off", "actual": "set by hand", "status": "Non-Compliant", "detail": "Wrong time breaks certificate checks and makes logs unreliable."}]},
|
| 114 |
+
"android": {"question": "Is 'Set time automatically' on?", "where": "Settings › System › Date & time", "expected": "set automatically",
|
| 115 |
+
"answers": [
|
| 116 |
+
{"label": "On", "actual": "set automatically", "status": "Compliant"},
|
| 117 |
+
{"label": "Off", "actual": "set by hand", "status": "Non-Compliant", "detail": "Wrong time breaks certificate checks and makes logs unreliable."}]}
|
| 118 |
+
},
|
| 119 |
+
"gdpr.backup_configured": {
|
| 120 |
+
"ios": {"question": "Is iCloud Backup on?", "where": "Settings › [your name] › iCloud › iCloud Backup", "expected": "backup on",
|
| 121 |
+
"answers": [
|
| 122 |
+
{"label": "On", "actual": "iCloud Backup on", "status": "Compliant"},
|
| 123 |
+
{"label": "Off, but I back up to a computer", "actual": "backed up to a computer", "status": "Compliant", "detail": "Manual computer backups depend on the user remembering them."},
|
| 124 |
+
{"label": "Off", "actual": "no backup", "status": "Non-Compliant", "detail": "Data on a lost or broken phone cannot be restored."}]},
|
| 125 |
+
"android": {"question": "Is phone backup on?", "where": "Settings › Google › Backup (or Settings › System › Backup; Samsung: Accounts and backup)", "expected": "backup on",
|
| 126 |
+
"answers": [
|
| 127 |
+
{"label": "Backup by Google One is on", "actual": "Google backup on", "status": "Compliant"},
|
| 128 |
+
{"label": "The phone maker's cloud backup is on", "actual": "phone maker cloud backup on", "status": "Compliant"},
|
| 129 |
+
{"label": "Off", "actual": "no backup", "status": "Non-Compliant", "detail": "Data on a lost or broken phone cannot be restored."}]}
|
| 130 |
+
},
|
| 131 |
+
"gdpr.telemetry": {
|
| 132 |
+
"ios": {"question": "Is 'Share iPhone Analytics' on?", "where": "Settings › Privacy & Security › Analytics & Improvements", "expected": "analytics sharing off",
|
| 133 |
+
"answers": [
|
| 134 |
+
{"label": "Off", "actual": "analytics sharing off", "status": "Compliant"},
|
| 135 |
+
{"label": "On", "actual": "analytics sharing on", "status": "Non-Compliant", "detail": "Usage data from the phone is sent to Apple."}]},
|
| 136 |
+
"android": {"question": "Is 'Usage & diagnostics' on?", "where": "Settings › Google › All services › Usage & diagnostics (also check the phone maker's diagnostics option)", "expected": "usage & diagnostics off",
|
| 137 |
+
"answers": [
|
| 138 |
+
{"label": "Off", "actual": "usage & diagnostics off", "status": "Compliant"},
|
| 139 |
+
{"label": "On", "actual": "usage & diagnostics on", "status": "Non-Compliant", "detail": "Usage data from the phone is sent to Google."}]}
|
| 140 |
+
},
|
| 141 |
+
"gdpr.lock_screen_previews": {
|
| 142 |
+
"ios": {"question": "What is 'Show Previews' set to?", "where": "Settings › Notifications › Show Previews", "expected": "When Unlocked or Never",
|
| 143 |
+
"answers": [
|
| 144 |
+
{"label": "When Unlocked", "actual": "When Unlocked", "status": "Compliant"},
|
| 145 |
+
{"label": "Never", "actual": "Never", "status": "Compliant"},
|
| 146 |
+
{"label": "Always", "actual": "Always", "status": "Non-Compliant", "detail": "Message and email content can be read on the locked screen."}]},
|
| 147 |
+
"android": {"question": "What do notifications show on the lock screen?", "where": "Settings › Notifications › Notifications on lock screen (or Settings › Privacy)", "expected": "sensitive content hidden",
|
| 148 |
+
"answers": [
|
| 149 |
+
{"label": "Don't show notifications", "actual": "no notifications on lock screen", "status": "Compliant"},
|
| 150 |
+
{"label": "Hide sensitive content / show only alerts", "actual": "sensitive content hidden", "status": "Compliant"},
|
| 151 |
+
{"label": "Show all notification content", "actual": "all content shown", "status": "Non-Compliant", "detail": "Message and email content can be read on the locked screen."}]}
|
| 152 |
+
},
|
| 153 |
+
"aiact.os_ai_features": {
|
| 154 |
+
"ios": {"question": "Is the ChatGPT extension in Apple Intelligence on?", "where": "Settings › Apple Intelligence & Siri › ChatGPT (only on iPhones that support Apple Intelligence)", "expected": "external AI off unless approved",
|
| 155 |
+
"answers": [
|
| 156 |
+
{"label": "Off", "actual": "ChatGPT extension off", "status": "Compliant"},
|
| 157 |
+
{"label": "On", "actual": "ChatGPT extension on", "status": "Non-Compliant", "detail": "Requests can be sent to an external AI service."},
|
| 158 |
+
{"label": "Apple Intelligence is not on this iPhone", "actual": "Apple Intelligence not available", "status": "NotApplicable"}]},
|
| 159 |
+
"android": {"question": "Can the AI assistant use what is on your screen?", "where": "Gemini app › your profile picture › Settings › 'Screen context' / 'Use screen context' (other assistants have similar options)", "expected": "screen access off unless approved",
|
| 160 |
+
"answers": [
|
| 161 |
+
{"label": "Off, or no AI assistant", "actual": "assistant screen access off", "status": "Compliant"},
|
| 162 |
+
{"label": "On", "actual": "assistant screen access on", "status": "Non-Compliant", "detail": "Screen content can be sent to an external AI service."}]}
|
| 163 |
+
}
|
| 164 |
+
},
|
| 165 |
+
"ai_apps": {
|
| 166 |
+
"question": "Which AI apps are installed on this phone?",
|
| 167 |
+
"where": "Swipe down on the Home Screen and search, or look in the App Library / app drawer.",
|
| 168 |
+
"options": ["ChatGPT", "Gemini", "Microsoft Copilot", "Claude", "Perplexity", "Meta AI", "Grok", "DeepSeek", "Character.AI", "Replika", "Otter", "Grammarly"]
|
| 169 |
+
}
|
| 170 |
+
}
|
collectors/linux/audit.sh
CHANGED
|
@@ -11,7 +11,7 @@
|
|
| 11 |
set -u
|
| 12 |
|
| 13 |
COLLECTOR_VERSION="0.2.0"
|
| 14 |
-
CATALOG_VERSION="0.
|
| 15 |
|
| 16 |
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
| 17 |
ROOT_DIR=$(cd "$SCRIPT_DIR/../.." && pwd)
|
|
|
|
| 11 |
set -u
|
| 12 |
|
| 13 |
COLLECTOR_VERSION="0.2.0"
|
| 14 |
+
CATALOG_VERSION="0.3.0"
|
| 15 |
|
| 16 |
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
| 17 |
ROOT_DIR=$(cd "$SCRIPT_DIR/../.." && pwd)
|
collectors/macos/audit.sh
CHANGED
|
@@ -13,7 +13,7 @@
|
|
| 13 |
set -u
|
| 14 |
|
| 15 |
COLLECTOR_VERSION="0.2.0"
|
| 16 |
-
CATALOG_VERSION="0.
|
| 17 |
|
| 18 |
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
| 19 |
ROOT_DIR=$(cd "$SCRIPT_DIR/../.." && pwd)
|
|
|
|
| 13 |
set -u
|
| 14 |
|
| 15 |
COLLECTOR_VERSION="0.2.0"
|
| 16 |
+
CATALOG_VERSION="0.3.0"
|
| 17 |
|
| 18 |
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
| 19 |
ROOT_DIR=$(cd "$SCRIPT_DIR/../.." && pwd)
|
collectors/windows/audit.ps1
CHANGED
|
@@ -26,7 +26,7 @@ param(
|
|
| 26 |
Set-StrictMode -Version 2.0
|
| 27 |
$ErrorActionPreference = 'Stop'
|
| 28 |
$CollectorVersion = '0.2.0'
|
| 29 |
-
$CatalogVersion = '0.
|
| 30 |
|
| 31 |
# ---------------------------------------------------------------------------
|
| 32 |
# Helpers
|
|
|
|
| 26 |
Set-StrictMode -Version 2.0
|
| 27 |
$ErrorActionPreference = 'Stop'
|
| 28 |
$CollectorVersion = '0.2.0'
|
| 29 |
+
$CatalogVersion = '0.3.0'
|
| 30 |
|
| 31 |
# ---------------------------------------------------------------------------
|
| 32 |
# Helpers
|
docs/CONTROLS.md
CHANGED
|
@@ -1,96 +1,103 @@
|
|
| 1 |
# Audited controls and data sources
|
| 2 |
|
| 3 |
-
Catalog version 0.
|
| 4 |
|
| 5 |
All sources are read-only. Internet is only used by *Pending security updates* on Windows and macOS, and by the app to refresh the vulnerability bundle.
|
|
|
|
| 6 |
|
| 7 |
-
## COMPLIANCE RESULTS (
|
| 8 |
|
| 9 |
-
| Check | Applies to | References | Rule | Windows source | Linux source | macOS source |
|
| 10 |
-
|---|---|---|---|---|---|---|
|
| 11 |
-
| Minimum password length | windows, linux, macos | ISO A.5.17; ISO A.8.5; GDPR Art. 32(1)(b) | Configured minimum length >= PASSWORD_MIN_LENGTH. | secedit export: MinimumPasswordLength | PAM pam_pwquality minlen (/etc/security/pwquality.conf, PAM args) | pwpolicy -getaccountpolicies (minLength / regex) |
|
| 12 |
-
| Password history | windows, linux, macos | ISO A.5.17; ISO A.8.5; GDPR Art. 32(1)(b) | Remembered passwords >= PASSWORD_HISTORY. | secedit export: PasswordHistorySize | pam_pwhistory remember (PAM args, /etc/security/pwhistory.conf) | pwpolicy: policyAttributePasswordHistoryDepth |
|
| 13 |
-
| Password expiration | windows, linux, macos | ISO A.5.17; GDPR Art. 32(1)(b) | Passwords expire after 1..PASSWORD_MAX_AGE_DAYS days (0 in the baseline = expiry not required). | secedit export: MaximumPasswordAge | /etc/login.defs PASS_MAX_DAYS + /etc/shadow per account | pwpolicy: policyAttributeExpiresEveryNDays |
|
| 14 |
-
| Minimum password age | windows, linux, macos | ISO A.5.17; GDPR Art. 32(1)(b) | Minimum age >= PASSWORD_MIN_AGE_DAYS days. macOS has no such setting (NotApplicable). | secedit export: MinimumPasswordAge | /etc/login.defs PASS_MIN_DAYS + /etc/shadow per account | not available on macOS (NotApplicable) |
|
| 15 |
-
| Password complexity | windows, linux, macos | ISO A.5.17; ISO A.8.5; GDPR Art. 32(1)(b) | Complexity enforced (mixed character classes). | secedit export: PasswordComplexity | pam_pwquality minclass / dcredit-ucredit-lcredit-ocredit | pwpolicy: alpha + numeric requirements |
|
| 16 |
-
| Account lockout threshold | windows, linux, macos | ISO A.8.5; GDPR Art. 32(1)(b) | Lockout after 1..LOCKOUT_THRESHOLD_MAX failed attempts (0 = never locks). | secedit export: LockoutBadCount | pam_faillock deny (PAM args, /etc/security/faillock.conf) | pwpolicy: policyAttributeMaximumFailedAuthentications |
|
| 17 |
-
| Account lockout duration | windows, linux, macos | ISO A.8.5; GDPR Art. 32(1)(b) | Locked for >= LOCKOUT_DURATION_MIN_MINUTES minutes, or until an administrator unlocks. | secedit export: LockoutDuration | pam_faillock unlock_time | pwpolicy: autoEnableInSeconds |
|
| 18 |
-
| Lockout observation period | windows, linux, macos | ISO A.8.5; GDPR Art. 32(1)(b) | Failed-attempt counter kept for >= LOCKOUT_OBSERVATION_MIN_MINUTES minutes. | secedit export: ResetLockoutCount | pam_faillock fail_interval | pwpolicy: autoEnableInSeconds (counter resets with lockout) |
|
| 19 |
-
| Antivirus | windows, linux, macos | ISO A.8.7; GDPR Art. 32(1)(b) | An enabled, up-to-date antivirus product is present. | Security Center (root/SecurityCenter2 AntiVirusProduct) | systemd services: clamav-daemon, mdatp, falcon-sensor, sentinelone, sophos-spl, esets | XProtect version (xprotect version) + running third-party agents |
|
| 20 |
-
| Malware protection service / Apple malware protection service / ClamAV malware protection service / Microsoft Defender Antimalware | windows, linux, macos | ISO A.8.7; GDPR Art. 32(1)(b) | The platform malware protection service is running. | Get-MpComputerStatus: AMServiceEnabled, AntivirusEnabled | systemctl is-active clamav-daemon / clamd | xprotect status + spctl --status (Gatekeeper) |
|
| 21 |
-
| Real-time protection | windows | ISO A.8.7; GDPR Art. 32(1)(b) | Defender real-time protection is on. | Get-MpComputerStatus: RealTimeProtectionEnabled | — | — |
|
| 22 |
-
| On-access malware protection / On-access protection | windows, linux, macos | ISO A.8.7; GDPR Art. 32(1)(b) | Files are scanned when opened. macOS: NotApplicable unless a third-party scanner provides it. | Get-MpComputerStatus: OnAccessProtectionEnabled | clamav-clamonacc service / clamonacc process | third-party agent only (XProtect has no on-access scan) |
|
| 23 |
-
| Tamper protection | windows | ISO A.8.7; GDPR Art. 32(1)(b) | Defender tamper protection is on. | Get-MpComputerStatus: IsTamperProtected | — | — |
|
| 24 |
-
| Malware signatures / Malware protection signatures / Security intelligence | windows, linux, macos | ISO A.8.7; ISO A.8.8; GDPR Art. 32(1)(b) | Signatures no older than SIGNATURE_MAX_AGE_DAYS (macOS XProtect: MACOS_XPROTECT_MAX_AGE_DAYS). | Get-MpComputerStatus: AntivirusSignatureAge | age of /var/lib/clamav/daily.c[lv]d | XProtect install date (xprotect version / XProtect.meta.plist) |
|
| 25 |
-
| Malware signature updates / Malware security updates | linux, macos | ISO A.8.7; ISO A.8.8; GDPR Art. 32(1)(b) | Automatic signature/security-data updates are enabled. | — | systemctl is-active clamav-freshclam | com.apple.SoftwareUpdate ConfigDataInstall + CriticalUpdateInstall |
|
| 26 |
-
| Firewall | linux, macos | ISO A.8.20; ISO A.8.1; GDPR Art. 32(1)(b) | A host firewall is active. | — | ufw status / firewalld / nft list ruleset / iptables -S | socketfilterfw --getglobalstate |
|
| 27 |
-
| Firewall inbound policy | linux, macos | ISO A.8.20; GDPR Art. 32(1)(b) | Unsolicited inbound traffic is blocked by default (macOS: firewall on with stealth mode). | — | ufw default incoming / firewalld zone target / nft input policy | socketfilterfw --getstealthmode |
|
| 28 |
-
| Firewall outbound policy | linux, macos | ISO A.8.20; GDPR Art. 32(1)(b) | Default outbound policy equals FIREWALL_OUTBOUND. | — | ufw default outgoing / nft output policy | application firewall is inbound-only |
|
| 29 |
-
| Domain firewall | windows | ISO A.8.20; ISO A.8.1; GDPR Art. 32(1)(b) | Domain profile enabled. | Get-NetFirewallProfile -PolicyStore ActiveStore (Domain) | — | — |
|
| 30 |
-
| Domain inbound | windows | ISO A.8.20; GDPR Art. 32(1)(b) | Domain profile default inbound action is Block. | Get-NetFirewallProfile: DefaultInboundAction (Domain) | — | — |
|
| 31 |
-
| Domain outbound | windows | ISO A.8.20; GDPR Art. 32(1)(b) | Domain profile default outbound action equals FIREWALL_OUTBOUND. | Get-NetFirewallProfile: DefaultOutboundAction (Domain) | — | — |
|
| 32 |
-
| Private firewall | windows | ISO A.8.20; ISO A.8.1; GDPR Art. 32(1)(b) | Private profile enabled. | Get-NetFirewallProfile -PolicyStore ActiveStore (Private) | — | — |
|
| 33 |
-
| Private inbound | windows | ISO A.8.20; GDPR Art. 32(1)(b) | Private profile default inbound action is Block. | Get-NetFirewallProfile: DefaultInboundAction (Private) | — | — |
|
| 34 |
-
| Private outbound | windows | ISO A.8.20; GDPR Art. 32(1)(b) | Private profile default outbound action equals FIREWALL_OUTBOUND. | Get-NetFirewallProfile: DefaultOutboundAction (Private) | — | — |
|
| 35 |
-
| Public firewall | windows | ISO A.8.20; ISO A.8.1; GDPR Art. 32(1)(b) | Public profile enabled. | Get-NetFirewallProfile -PolicyStore ActiveStore (Public) | — | — |
|
| 36 |
-
| Public inbound | windows | ISO A.8.20; GDPR Art. 32(1)(b) | Public profile default inbound action is Block. | Get-NetFirewallProfile: DefaultInboundAction (Public) | — | — |
|
| 37 |
-
| Public outbound | windows | ISO A.8.20; GDPR Art. 32(1)(b) | Public profile default outbound action equals FIREWALL_OUTBOUND. | Get-NetFirewallProfile: DefaultOutboundAction (Public) | — | — |
|
| 38 |
-
| Screen lock | windows, linux, macos | ISO A.8.1; ISO A.7.7; GDPR Art. 32(1)(b) | The screen locks when idle (Windows: machine inactivity limit or a password-protected screen saver). | InactivityTimeoutSecs policy or users' ScreenSaveActive/ScreenSaverIsSecure (HKU) | gsettings org.gnome.desktop.screensaver lock-enabled + session idle-delay (per user) | sysadminctl -screenLock status / managed com.apple.screensaver |
|
| 39 |
-
| Screen lock timeout | windows, linux, macos | ISO A.8.1; ISO A.7.7; GDPR Art. 32(1)(b) | Idle time before lock (including any grace delay) is 1..SCREEN_LOCK_MAX_SECONDS seconds (baseline: 300 = 5 minutes). | InactivityTimeoutSecs policy or users' ScreenSaveTimeOut (HKU) | gsettings idle-delay + lock-delay (per user) | screen saver idleTime + pmset displaysleep + password delay |
|
| 40 |
-
| Screen lock password requirement | windows, linux, macos | ISO A.8.1; ISO A.7.7; ISO A.8.5; GDPR Art. 32(1)(b) | A password is required to unlock (macOS: within 5 seconds of the screen locking). | InactivityTimeoutSecs policy or users' ScreenSaverIsSecure; DisableLockWorkstation | gsettings lock-enabled + lockdown disable-lock-screen | sysadminctl -screenLock delay / managed askForPassword |
|
| 41 |
-
|
|
| 42 |
-
|
|
| 43 |
-
|
|
| 44 |
-
|
|
| 45 |
-
|
|
| 46 |
-
|
|
| 47 |
-
|
|
| 48 |
-
|
|
| 49 |
-
|
|
| 50 |
-
|
|
| 51 |
-
|
|
| 52 |
-
|
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
-
## GDPR TECHNICAL CONTROLS (
|
| 56 |
|
| 57 |
-
| Check | Applies to | References | Rule | Windows source | Linux source | macOS source |
|
| 58 |
-
|---|---|---|---|---|---|---|
|
| 59 |
-
| Automatic security updates | windows, linux, macos | ISO A.8.8; GDPR Art. 32(1)(b) | Security updates are downloaded and installed automatically. | WindowsUpdate\AU policy (NoAutoUpdate, AUOptions), wuauserv start type | apt-config Unattended-Upgrade / dnf-automatic timer | com.apple.SoftwareUpdate AutomaticCheck/Download/Install keys |
|
| 60 |
-
| Security updates installed recently | windows, linux, macos | ISO A.8.8; GDPR Art. 32(1)(b); GDPR Art. 32(1)(d) | Last successful update/package change within UPDATES_MAX_AGE_DAYS days. | Windows Update history (COM) or Get-HotFix | /var/log/apt/history.log / rpm -qa --last | /Library/Receipts/InstallHistory.plist (softwareupdated) |
|
| 61 |
-
| Supported operating system version | windows, linux, macos | ISO A.8.8; GDPR Art. 32(1)(b) | The OS version still receives security updates (minimums in the baseline). | Win32_OperatingSystem BuildNumber vs WINDOWS_MIN_BUILD | /etc/os-release VERSION_ID vs baseline minimum | sw_vers -productVersion vs MACOS_MIN_MAJOR |
|
| 62 |
-
| Security event logging | windows, linux, macos | ISO A.8.15; GDPR Art. 32(1)(d); GDPR Art. 33 | Logon and account-change events are recorded (Windows audit policy, Linux auditd, macOS unified log). | auditpol /backup (Logon, User Account Management) + legacy audit policy | systemctl is-active auditd, journal persistence | logd running (unified logging) |
|
| 63 |
-
| Time synchronisation | windows, linux, macos | ISO A.8.17; GDPR Art. 33 | The clock is synchronised from a time server. | W32Time Parameters Type + service start type | timedatectl NTP / chronyd / ntpd | systemsetup -getusingnetworktime (needs root) |
|
| 64 |
-
| Backup configured | windows, linux, macos | ISO A.8.13; GDPR Art. 32(1)(c) | A backup solution is configured for user data. | OneDrive KFM policy, File History config, backup agent services | Timeshift config, Deja Dup (gsettings), restic/borg timers | tmutil destinationinfo, backup agent processes |
|
| 65 |
-
| Recent backup | windows, linux, macos | ISO A.8.13; GDPR Art. 32(1)(c) | Last backup within BACKUP_MAX_AGE_DAYS days (Pending when the date cannot be read). | not readable for most tools (Pending) | Timeshift snapshot names, Deja Dup last-backup | tmutil latestbackup |
|
| 66 |
-
| Removable media encryption | windows, linux, macos | ISO A.7.10; ISO A.8.24; GDPR Art. 32(1)(a); GDPR Art. 34 | Data cannot be written to unencrypted removable drives. | FVE RDVDenyWriteAccess (BitLocker To Go) or USB write denied | USB write denied | USB write denied |
|
| 67 |
-
| Guest account disabled | windows, linux, macos | ISO A.5.16; ISO A.8.5; GDPR Art. 32(1)(b) | No enabled guest login. | Get-LocalUser (SID -501): Enabled | LightDM allow-guest, 'guest' account shell | com.apple.loginwindow GuestEnabled, SMB AllowGuestAccess |
|
| 68 |
-
| Remote access services restricted | windows, linux, macos | ISO A.8.20; ISO A.6.7; GDPR Art. 32(1)(b) | Only remote access services listed in APPROVED_REMOTE_SERVICES are enabled. | fDenyTSConnections (RDP), sshd/WinRM/TeamViewer/AnyDesk/chromoting services, non-admin SMB shares | ssh/xrdp/vnc/samba/teamviewer/anydesk/chrome-remote-desktop | launchctl print-disabled (sshd, screensharing, smbd, chromoting), ARDAgent/TeamViewer/AnyDesk |
|
| 69 |
-
| Diagnostic data minimised | windows, linux, macos | ISO A.5.34; GDPR Art. 5(1)(c); GDPR Art. 25 | OS diagnostic/usage data sharing is at the minimum level. | DataCollection AllowTelemetry (policy / Settings) | popularity-contest, whoopsie | DiagnosticMessagesHistory AutoSubmit, ThirdPartyDataSubmit |
|
| 70 |
-
|
|
|
|
|
| 71 |
|
| 72 |
## EU AI ACT CONTROLS (5)
|
| 73 |
|
| 74 |
-
| Check | Applies to | References | Rule | Windows source | Linux source | macOS source |
|
| 75 |
-
|---|---|---|---|---|---|---|
|
| 76 |
-
| AI software inventory | windows, linux, macos | ISO A.5.9; GDPR Art. 30; AI Act Art. 26 | AI apps, local AI services and local model folders are listed (Compliant = inventory taken). | uninstall registry + Store apps (AI apps), port 11434, model folders, Claude Code CLI | AI CLIs, snaps, flatpaks, /opt apps, port 11434, model folders | /Applications AI apps, AI CLIs, port 11434, model folders |
|
| 77 |
-
| Unapproved AI tools (shadow AI) | windows, linux, macos | ISO A.5.9; ISO A.8.19; GDPR Art. 28; GDPR Art. 44; AI Act Art. 26 | Every AI tool found is listed in APPROVED_AI_TOOLS. | AI inventory vs APPROVED_AI_TOOLS | AI inventory vs APPROVED_AI_TOOLS | AI inventory vs APPROVED_AI_TOOLS |
|
| 78 |
-
| AI system risk classification | windows, linux, macos | ISO A.5.9; AI Act Art. 5; AI Act Art. 6; AI Act Annex III | Each AI tool found is classified (prohibited / high-risk / limited / minimal). Done by the auditor model: Pending in the collector output, NotApplicable when no AI tool is found. | AI inventory, classified by the model | AI inventory, classified by the model | AI inventory, classified by the model |
|
| 79 |
-
| Operating system AI features restricted / Apple Intelligence external integrations restricted / Windows Recall / Copilot data analysis disabled | windows, linux, macos | ISO A.5.34; GDPR Art. 5(1)(c); GDPR Art. 25; AI Act Art. 26 | OS-level AI features that capture or send user data are disabled by policy. NotApplicable where the OS has none. | WindowsAI policies (DisableAIDataAnalysis, AllowRecallEnablement), Recall optional feature | none built in (NotApplicable) | managed com.apple.applicationaccess allowExternalIntelligenceIntegrations (Apple silicon) |
|
| 80 |
-
| AI usage log retention | windows, linux, macos | ISO A.8.15; AI Act Art. 26(6) | Logs kept >= 6 months if a high-risk AI system is used. Pending until classification; NotApplicable when no AI tool is found. | depends on AI classification | depends on AI classification | depends on AI classification |
|
| 81 |
|
| 82 |
## ORGANISATIONAL CONTROLS (8)
|
| 83 |
|
| 84 |
-
| Check | Applies to | References | Rule | Windows source | Linux source | macOS source |
|
| 85 |
-
|---|---|---|---|---|---|---|
|
| 86 |
-
| Record of processing activities | windows, linux, macos | ISO A.5.34; GDPR Art. 30 | Answer yes = Compliant, no/partial = Non-Compliant, n/a = NotApplicable, unanswered = Pending. | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) |
|
| 87 |
-
| Data protection impact assessment | windows, linux, macos | ISO A.5.34; GDPR Art. 35 | As above. | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) |
|
| 88 |
-
| Breach response procedure | windows, linux, macos | ISO A.5.24; ISO A.5.26; GDPR Art. 33; GDPR Art. 34 | As above. | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) |
|
| 89 |
-
| Data processing agreements | windows, linux, macos | ISO A.5.20; GDPR Art. 28 | As above. | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) |
|
| 90 |
-
| AI register | windows, linux, macos | ISO A.5.9; AI Act Art. 26 | As above. | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) |
|
| 91 |
-
| AI literacy training | windows, linux, macos | ISO A.6.3; AI Act Art. 4 | As above. | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) |
|
| 92 |
-
| Human oversight for high-risk AI | windows, linux, macos | AI Act Art. 14; AI Act Art. 26 | As above. | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) |
|
| 93 |
-
| AI transparency to users | windows, linux, macos | AI Act Art. 50 | As above. | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) |
|
| 94 |
|
| 95 |
## Reference titles
|
| 96 |
|
|
|
|
| 1 |
# Audited controls and data sources
|
| 2 |
|
| 3 |
+
Catalog version 0.3.0 · 74 checks. Generated by `tools/build_controls_doc.py` — do not edit by hand.
|
| 4 |
|
| 5 |
All sources are read-only. Internet is only used by *Pending security updates* on Windows and macOS, and by the app to refresh the vulnerability bundle.
|
| 6 |
+
Phones are checked in the browser: a web page cannot read a phone's settings, so results are either detected by the browser or answered by the user (self-reported), and labelled as such.
|
| 7 |
|
| 8 |
+
## COMPLIANCE RESULTS (48)
|
| 9 |
|
| 10 |
+
| Check | Applies to | References | Rule | Windows source | Linux source | macOS source | iPhone source | Android source |
|
| 11 |
+
|---|---|---|---|---|---|---|---|---|
|
| 12 |
+
| Minimum password length | windows, linux, macos | ISO A.5.17; ISO A.8.5; GDPR Art. 32(1)(b) | Configured minimum length >= PASSWORD_MIN_LENGTH. | secedit export: MinimumPasswordLength | PAM pam_pwquality minlen (/etc/security/pwquality.conf, PAM args) | pwpolicy -getaccountpolicies (minLength / regex) | — | — |
|
| 13 |
+
| Password history | windows, linux, macos | ISO A.5.17; ISO A.8.5; GDPR Art. 32(1)(b) | Remembered passwords >= PASSWORD_HISTORY. | secedit export: PasswordHistorySize | pam_pwhistory remember (PAM args, /etc/security/pwhistory.conf) | pwpolicy: policyAttributePasswordHistoryDepth | — | — |
|
| 14 |
+
| Password expiration | windows, linux, macos | ISO A.5.17; GDPR Art. 32(1)(b) | Passwords expire after 1..PASSWORD_MAX_AGE_DAYS days (0 in the baseline = expiry not required). | secedit export: MaximumPasswordAge | /etc/login.defs PASS_MAX_DAYS + /etc/shadow per account | pwpolicy: policyAttributeExpiresEveryNDays | — | — |
|
| 15 |
+
| Minimum password age | windows, linux, macos | ISO A.5.17; GDPR Art. 32(1)(b) | Minimum age >= PASSWORD_MIN_AGE_DAYS days. macOS has no such setting (NotApplicable). | secedit export: MinimumPasswordAge | /etc/login.defs PASS_MIN_DAYS + /etc/shadow per account | not available on macOS (NotApplicable) | — | — |
|
| 16 |
+
| Password complexity | windows, linux, macos | ISO A.5.17; ISO A.8.5; GDPR Art. 32(1)(b) | Complexity enforced (mixed character classes). | secedit export: PasswordComplexity | pam_pwquality minclass / dcredit-ucredit-lcredit-ocredit | pwpolicy: alpha + numeric requirements | — | — |
|
| 17 |
+
| Account lockout threshold | windows, linux, macos | ISO A.8.5; GDPR Art. 32(1)(b) | Lockout after 1..LOCKOUT_THRESHOLD_MAX failed attempts (0 = never locks). | secedit export: LockoutBadCount | pam_faillock deny (PAM args, /etc/security/faillock.conf) | pwpolicy: policyAttributeMaximumFailedAuthentications | — | — |
|
| 18 |
+
| Account lockout duration | windows, linux, macos | ISO A.8.5; GDPR Art. 32(1)(b) | Locked for >= LOCKOUT_DURATION_MIN_MINUTES minutes, or until an administrator unlocks. | secedit export: LockoutDuration | pam_faillock unlock_time | pwpolicy: autoEnableInSeconds | — | — |
|
| 19 |
+
| Lockout observation period | windows, linux, macos | ISO A.8.5; GDPR Art. 32(1)(b) | Failed-attempt counter kept for >= LOCKOUT_OBSERVATION_MIN_MINUTES minutes. | secedit export: ResetLockoutCount | pam_faillock fail_interval | pwpolicy: autoEnableInSeconds (counter resets with lockout) | — | — |
|
| 20 |
+
| Antivirus / Google Play Protect | windows, linux, macos, android | ISO A.8.7; GDPR Art. 32(1)(b) | An enabled, up-to-date antivirus product is present. Android: Google Play Protect scanning is on. | Security Center (root/SecurityCenter2 AntiVirusProduct) | systemd services: clamav-daemon, mdatp, falcon-sensor, sentinelone, sophos-spl, esets | XProtect version (xprotect version) + running third-party agents | — | asked: Play Store › your profile picture › Play Protect › Settings (gear icon) |
|
| 21 |
+
| Malware protection service / Apple malware protection service / ClamAV malware protection service / Microsoft Defender Antimalware | windows, linux, macos | ISO A.8.7; GDPR Art. 32(1)(b) | The platform malware protection service is running. | Get-MpComputerStatus: AMServiceEnabled, AntivirusEnabled | systemctl is-active clamav-daemon / clamd | xprotect status + spctl --status (Gatekeeper) | — | — |
|
| 22 |
+
| Real-time protection | windows | ISO A.8.7; GDPR Art. 32(1)(b) | Defender real-time protection is on. | Get-MpComputerStatus: RealTimeProtectionEnabled | — | — | — | — |
|
| 23 |
+
| On-access malware protection / On-access protection | windows, linux, macos | ISO A.8.7; GDPR Art. 32(1)(b) | Files are scanned when opened. macOS: NotApplicable unless a third-party scanner provides it. | Get-MpComputerStatus: OnAccessProtectionEnabled | clamav-clamonacc service / clamonacc process | third-party agent only (XProtect has no on-access scan) | — | — |
|
| 24 |
+
| Tamper protection | windows | ISO A.8.7; GDPR Art. 32(1)(b) | Defender tamper protection is on. | Get-MpComputerStatus: IsTamperProtected | — | — | — | — |
|
| 25 |
+
| Malware signatures / Malware protection signatures / Security intelligence | windows, linux, macos | ISO A.8.7; ISO A.8.8; GDPR Art. 32(1)(b) | Signatures no older than SIGNATURE_MAX_AGE_DAYS (macOS XProtect: MACOS_XPROTECT_MAX_AGE_DAYS). | Get-MpComputerStatus: AntivirusSignatureAge | age of /var/lib/clamav/daily.c[lv]d | XProtect install date (xprotect version / XProtect.meta.plist) | — | — |
|
| 26 |
+
| Malware signature updates / Malware security updates | linux, macos | ISO A.8.7; ISO A.8.8; GDPR Art. 32(1)(b) | Automatic signature/security-data updates are enabled. | — | systemctl is-active clamav-freshclam | com.apple.SoftwareUpdate ConfigDataInstall + CriticalUpdateInstall | — | — |
|
| 27 |
+
| Firewall | linux, macos | ISO A.8.20; ISO A.8.1; GDPR Art. 32(1)(b) | A host firewall is active. | — | ufw status / firewalld / nft list ruleset / iptables -S | socketfilterfw --getglobalstate | — | — |
|
| 28 |
+
| Firewall inbound policy | linux, macos | ISO A.8.20; GDPR Art. 32(1)(b) | Unsolicited inbound traffic is blocked by default (macOS: firewall on with stealth mode). | — | ufw default incoming / firewalld zone target / nft input policy | socketfilterfw --getstealthmode | — | — |
|
| 29 |
+
| Firewall outbound policy | linux, macos | ISO A.8.20; GDPR Art. 32(1)(b) | Default outbound policy equals FIREWALL_OUTBOUND. | — | ufw default outgoing / nft output policy | application firewall is inbound-only | — | — |
|
| 30 |
+
| Domain firewall | windows | ISO A.8.20; ISO A.8.1; GDPR Art. 32(1)(b) | Domain profile enabled. | Get-NetFirewallProfile -PolicyStore ActiveStore (Domain) | — | — | — | — |
|
| 31 |
+
| Domain inbound | windows | ISO A.8.20; GDPR Art. 32(1)(b) | Domain profile default inbound action is Block. | Get-NetFirewallProfile: DefaultInboundAction (Domain) | — | — | — | — |
|
| 32 |
+
| Domain outbound | windows | ISO A.8.20; GDPR Art. 32(1)(b) | Domain profile default outbound action equals FIREWALL_OUTBOUND. | Get-NetFirewallProfile: DefaultOutboundAction (Domain) | — | — | — | — |
|
| 33 |
+
| Private firewall | windows | ISO A.8.20; ISO A.8.1; GDPR Art. 32(1)(b) | Private profile enabled. | Get-NetFirewallProfile -PolicyStore ActiveStore (Private) | — | — | — | — |
|
| 34 |
+
| Private inbound | windows | ISO A.8.20; GDPR Art. 32(1)(b) | Private profile default inbound action is Block. | Get-NetFirewallProfile: DefaultInboundAction (Private) | — | — | — | — |
|
| 35 |
+
| Private outbound | windows | ISO A.8.20; GDPR Art. 32(1)(b) | Private profile default outbound action equals FIREWALL_OUTBOUND. | Get-NetFirewallProfile: DefaultOutboundAction (Private) | — | — | — | — |
|
| 36 |
+
| Public firewall | windows | ISO A.8.20; ISO A.8.1; GDPR Art. 32(1)(b) | Public profile enabled. | Get-NetFirewallProfile -PolicyStore ActiveStore (Public) | — | — | — | — |
|
| 37 |
+
| Public inbound | windows | ISO A.8.20; GDPR Art. 32(1)(b) | Public profile default inbound action is Block. | Get-NetFirewallProfile: DefaultInboundAction (Public) | — | — | — | — |
|
| 38 |
+
| Public outbound | windows | ISO A.8.20; GDPR Art. 32(1)(b) | Public profile default outbound action equals FIREWALL_OUTBOUND. | Get-NetFirewallProfile: DefaultOutboundAction (Public) | — | — | — | — |
|
| 39 |
+
| Screen lock / Passcode | windows, linux, macos, ios, android | ISO A.8.1; ISO A.7.7; GDPR Art. 32(1)(b) | The screen locks when idle (Windows: machine inactivity limit or a password-protected screen saver). Phones: a passcode / PIN / password screen lock is set. | InactivityTimeoutSecs policy or users' ScreenSaveActive/ScreenSaverIsSecure (HKU) | gsettings org.gnome.desktop.screensaver lock-enabled + session idle-delay (per user) | sysadminctl -screenLock status / managed com.apple.screensaver | detected: WebAuthn platform authenticator available; otherwise asked; asked: Settings › Face ID & Passcode (or Touch ID & Passcode). If it asks for your passcode before opening, one is set. | detected: WebAuthn platform authenticator available; otherwise asked; asked: Settings › Security & privacy › Device unlock › Screen lock (names vary by phone maker) |
|
| 40 |
+
| Screen lock timeout / Auto-Lock / Screen timeout | windows, linux, macos, ios, android | ISO A.8.1; ISO A.7.7; GDPR Art. 32(1)(b) | Idle time before lock (including any grace delay) is 1..SCREEN_LOCK_MAX_SECONDS seconds (baseline: 300 = 5 minutes). Phones: Auto-Lock / screen timeout. | InactivityTimeoutSecs policy or users' ScreenSaveTimeOut (HKU) | gsettings idle-delay + lock-delay (per user) | screen saver idleTime + pmset displaysleep + password delay | asked: Settings › Display & Brightness › Auto-Lock | asked: Settings › Display › Screen timeout. If there is also 'Lock after screen timeout', it should be Immediately. |
|
| 41 |
+
| Screen lock password requirement | windows, linux, macos | ISO A.8.1; ISO A.7.7; ISO A.8.5; GDPR Art. 32(1)(b) | A password is required to unlock (macOS: within 5 seconds of the screen locking). | InactivityTimeoutSecs policy or users' ScreenSaverIsSecure; DisableLockWorkstation | gsettings lock-enabled + lockdown disable-lock-screen | sysadminctl -screenLock delay / managed askForPassword | — | — |
|
| 42 |
+
| Passcode strength / Screen lock strength | ios, android | ISO A.5.17; ISO A.8.5; GDPR Art. 32(1)(b) | Passcode / PIN has at least MOBILE_PASSCODE_MIN_DIGITS digits, or is alphanumeric. Android pattern or swipe locks do not comply. | — | — | — | asked: The code you type to unlock. Settings › Face ID & Passcode › Change Passcode › Passcode Options shows the choices. | asked: Settings › Security & privacy › Device unlock › Screen lock |
|
| 43 |
+
| Operating system integrity / Not jailbroken / Not rooted | ios, android | ISO A.8.1; ISO A.8.7; ISO A.8.19; GDPR Art. 32(1)(b) | The phone is not jailbroken (iOS) or rooted / bootloader-unlocked (Android). | — | — | — | asked: Jailbreaking means deliberately modifying iOS to install apps outside Apple's control. If nobody did this on purpose, answer No. | asked: Rooting means deliberately giving apps full system access. If nobody did this on purpose, answer No. |
|
| 44 |
+
| App sources restricted / Apps only from the App Store / Install unknown apps blocked | ios, android | ISO A.8.1; ISO A.8.7; ISO A.8.19; GDPR Art. 32(1)(b) | Apps are installed only from the official store (no alternative marketplaces, web distribution or unknown sources). | — | — | — | asked: Settings › General › VPN & Device Management lists developer or enterprise profiles. Alternative app marketplaces appear as separate apps. | asked: Settings › Apps › Special app access › Install unknown apps. Every app should show 'Not allowed'. |
|
| 45 |
+
| USB debugging off | android | ISO A.8.1; ISO A.8.19; GDPR Art. 32(1)(b) | Developer options / USB debugging are off. | — | — | — | — | asked: Settings › System › Developer options › USB debugging. If you cannot find Developer options, they are off. |
|
| 46 |
+
| Find and erase a lost device / Find My Device / Find My iPhone | ios, android | ISO A.5.9; ISO A.8.1; GDPR Art. 32(1)(b); GDPR Art. 32(1)(c) | Find My iPhone / Find My Device is on, so a lost phone can be located, locked and erased. | — | — | — | asked: Settings › [your name] › Find My › Find My iPhone | asked: Settings › Google › All services › Find My Device (called Find Hub on newer phones) |
|
| 47 |
+
| USB storage device access | windows, linux, macos | ISO A.7.10; ISO A.8.12; GDPR Art. 32(1)(b) | Whether USB mass storage devices can connect at all equals USB_DEVICE_ACCESS (allow/deny): driver, device-installation policy, USBGuard or MDM restriction. | RemovableStorageDevices Deny_All policy, USBSTOR service Start | modprobe usb-storage install rule / blacklist / USBGuard | managed com.apple.systemuiserver mount-controls | — | — |
|
| 48 |
+
| USB storage read | windows, linux, macos | ISO A.7.10; ISO A.8.12; GDPR Art. 32(1)(b) | Effective USB storage read access equals USB_READ (allow/deny). | RemovableStorageDevices policies (Deny_All, Deny_Read), USBSTOR Start | usb-storage module rules, USBGuard | managed mount-controls (deny) | — | — |
|
| 49 |
+
| USB storage write | windows, linux, macos | ISO A.7.10; ISO A.8.12; GDPR Art. 32(1)(b) | Effective USB storage write access equals USB_WRITE (allow/deny). | RemovableStorageDevices Deny_Write, StorageDevicePolicies WriteProtect | udev read-only rules, usb-storage rules, USBGuard | managed mount-controls (deny / read-only) | — | — |
|
| 50 |
+
| Approved IT administrator | windows, linux, macos | ISO A.8.2; ISO A.5.15; GDPR Art. 32(1)(b) | At least one account listed in APPROVED_ADMINS is an enabled administrator. | Administrators group by SID S-1-5-32-544 (ADSI) + Get-LocalUser | getent group sudo/admin/wheel, UID 0 accounts, /etc/sudoers(.d) | dscl . -read /Groups/admin GroupMembership | — | — |
|
| 51 |
+
| Unauthorized employee administrator accounts / Unauthorized employee local administrator accounts | windows, linux, macos | ISO A.8.2; ISO A.5.18; GDPR Art. 32(1)(b) | No administrator accounts other than APPROVED_ADMINS (and the disabled built-in Administrator on Windows). | Administrators group by SID S-1-5-32-544 (ADSI) + Get-LocalUser | getent group sudo/admin/wheel, UID 0 accounts, /etc/sudoers(.d) | dscl . -read /Groups/admin GroupMembership | — | — |
|
| 52 |
+
| BitLocker | windows | ISO A.8.24; ISO A.8.1; GDPR Art. 32(1)(a) | BitLocker is turned on for the system drive. | Get-BitLockerVolume (system drive): VolumeStatus | — | — | — | — |
|
| 53 |
+
| Encryption / Data protection (encryption) / Device encryption | windows, linux, macos, ios, android | ISO A.8.24; ISO A.8.1; GDPR Art. 32(1)(a) | The system disk is fully encrypted (BitLocker / LUKS / FileVault). Phones: iOS data protection is on when a passcode is set; Android 10+ encrypts by default. | Get-BitLockerVolume: VolumeStatus, EncryptionPercentage | findmnt / + lsblk (crypt device under /) | fdesetup status | derived: on when a passcode is set (iOS data protection) | detected: Android 10+ encrypts by default |
|
| 54 |
+
| Disk encryption protection / BitLocker protection | windows, linux, macos | ISO A.8.24; GDPR Art. 32(1)(a) | Encryption protection is active (not suspended or in progress) with a strong cipher. | Get-BitLockerVolume: ProtectionStatus, EncryptionMethod | cryptsetup status (type, cipher, key size) | fdesetup status (no encryption in progress) | — | — |
|
| 55 |
+
| Disk encryption recovery / BitLocker recovery | windows, linux, macos | ISO A.8.24; ISO A.8.13; GDPR Art. 32(1)(a); GDPR Art. 32(1)(c) | A recovery key or second unlock method exists. | Get-BitLockerVolume: KeyProtector (RecoveryPassword) | cryptsetup luksDump (key slots, tokens) | fdesetup haspersonalrecoverykey / hasinstitutionalrecoverykey | — | — |
|
| 56 |
+
| Signed-in user account type | windows, linux, macos | ISO A.8.2; ISO A.5.15; GDPR Art. 32(1)(b) | The everyday signed-in user is a Standard account (not an administrator), unless listed in APPROVED_ADMINS. | signed-in user (Win32_ComputerSystem) vs Administrators group | session users (loginctl) vs sudo/admin/wheel | console user (stat /dev/console) + dsmemberutil checkmembership | — | — |
|
| 57 |
+
| Pending security updates / Android security update / iOS security updates | windows, linux, macos, ios, android | ISO A.8.8; GDPR Art. 32(1)(b); GDPR Art. 32(1)(d) | Security updates offered by the OS vendor but not yet installed <= MAX_PENDING_SECURITY_UPDATES. Phones: iOS is at the latest security release known to the vulnerability data; Android security patch level is at most MOBILE_PATCH_MAX_AGE_DAYS old. | Windows Update Agent API (Microsoft.Update.Session search) - needs internet | apt-get -s upgrade (security) / dnf updateinfo --security - uses local package lists | softwareupdate -l - needs internet | detected: iOS version vs newest fixed release in the signed phone bundle | asked: Settings › About phone › Android version › Android security update (or Settings › Security & privacy › Updates) |
|
| 58 |
+
| Outdated third-party applications / App updates | windows, linux, macos, ios, android | ISO A.8.8; ISO A.8.19; GDPR Art. 32(1)(b) | Installed third-party applications with a newer version available <= MAX_OUTDATED_APPS (Windows: winget; macOS: app versions; Linux: snap/flatpak and packages). Phones: automatic app updates are on. | winget upgrade | apt upgradable, snap refresh --list, flatpak updates | brew outdated (Homebrew; App Store apps not covered) | asked: Settings › App Store › App Updates | asked: Play Store › your profile picture › Settings › Network preferences › Auto-update apps |
|
| 59 |
+
| Known vulnerabilities in installed software / Known vulnerabilities in iOS | windows, linux, macos, ios | ISO A.8.8; GDPR Art. 32(1)(b) | Installed software versions matched against an offline copy of the OSV/NVD vulnerability data: critical or high vulnerabilities <= MAX_CRITICAL_HIGH_VULNERABILITIES. iOS: the iOS version is matched against NVD iOS/iPadOS records. | installed programs (machine and per-user uninstall registry) matched by the app against the signed offline bundle (NVD ranges, CISA KEV, EPSS) | dpkg/rpm package list (with source packages) matched by the app against the signed offline bundle (OSV: Ubuntu, Debian, RHEL, AlmaLinux, Rocky) | app bundle versions (/Applications, ~/Applications) matched by the app against the signed offline bundle (NVD ranges, CISA KEV, EPSS) | detected: iOS version matched against NVD iOS/iPadOS records in the signed phone bundle | — |
|
| 60 |
|
| 61 |
+
## GDPR TECHNICAL CONTROLS (13)
|
| 62 |
|
| 63 |
+
| Check | Applies to | References | Rule | Windows source | Linux source | macOS source | iPhone source | Android source |
|
| 64 |
+
|---|---|---|---|---|---|---|---|---|
|
| 65 |
+
| Automatic security updates / Automatic iOS updates / Automatic system updates | windows, linux, macos, ios, android | ISO A.8.8; GDPR Art. 32(1)(b) | Security updates are downloaded and installed automatically. | WindowsUpdate\AU policy (NoAutoUpdate, AUOptions), wuauserv start type | apt-config Unattended-Upgrade / dnf-automatic timer | com.apple.SoftwareUpdate AutomaticCheck/Download/Install keys | asked: Settings › General › Software Update › Automatic Updates | asked: Settings › System › Software updates (names vary: 'Auto download over Wi-Fi', 'Automatic system updates') |
|
| 66 |
+
| Security updates installed recently | windows, linux, macos | ISO A.8.8; GDPR Art. 32(1)(b); GDPR Art. 32(1)(d) | Last successful update/package change within UPDATES_MAX_AGE_DAYS days. | Windows Update history (COM) or Get-HotFix | /var/log/apt/history.log / rpm -qa --last | /Library/Receipts/InstallHistory.plist (softwareupdated) | — | — |
|
| 67 |
+
| Supported operating system version | windows, linux, macos, ios, android | ISO A.8.8; GDPR Art. 32(1)(b) | The OS version still receives security updates (minimums in the baseline). Phones: IOS_MIN_MAJOR / ANDROID_MIN_MAJOR. | Win32_OperatingSystem BuildNumber vs WINDOWS_MIN_BUILD | /etc/os-release VERSION_ID vs baseline minimum | sw_vers -productVersion vs MACOS_MIN_MAJOR | detected: OS version (Safari version on iOS 26+, client hints on Android), confirmed by the user | detected: OS version (Safari version on iOS 26+, client hints on Android), confirmed by the user |
|
| 68 |
+
| Security event logging | windows, linux, macos | ISO A.8.15; GDPR Art. 32(1)(d); GDPR Art. 33 | Logon and account-change events are recorded (Windows audit policy, Linux auditd, macOS unified log). | auditpol /backup (Logon, User Account Management) + legacy audit policy | systemctl is-active auditd, journal persistence | logd running (unified logging) | — | — |
|
| 69 |
+
| Time synchronisation / Date & time set automatically | windows, linux, macos, ios, android | ISO A.8.17; GDPR Art. 33 | The clock is synchronised from a time server. | W32Time Parameters Type + service start type | timedatectl NTP / chronyd / ntpd | systemsetup -getusingnetworktime (needs root) | asked: Settings › General › Date & Time | asked: Settings › System › Date & time |
|
| 70 |
+
| Backup configured / Google backup / iCloud Backup | windows, linux, macos, ios, android | ISO A.8.13; GDPR Art. 32(1)(c) | A backup solution is configured for user data. | OneDrive KFM policy, File History config, backup agent services | Timeshift config, Deja Dup (gsettings), restic/borg timers | tmutil destinationinfo, backup agent processes | asked: Settings › [your name] › iCloud › iCloud Backup | asked: Settings › Google › Backup (or Settings › System › Backup; Samsung: Accounts and backup) |
|
| 71 |
+
| Recent backup | windows, linux, macos | ISO A.8.13; GDPR Art. 32(1)(c) | Last backup within BACKUP_MAX_AGE_DAYS days (Pending when the date cannot be read). | not readable for most tools (Pending) | Timeshift snapshot names, Deja Dup last-backup | tmutil latestbackup | — | — |
|
| 72 |
+
| Removable media encryption | windows, linux, macos | ISO A.7.10; ISO A.8.24; GDPR Art. 32(1)(a); GDPR Art. 34 | Data cannot be written to unencrypted removable drives. | FVE RDVDenyWriteAccess (BitLocker To Go) or USB write denied | USB write denied | USB write denied | — | — |
|
| 73 |
+
| Guest account disabled | windows, linux, macos | ISO A.5.16; ISO A.8.5; GDPR Art. 32(1)(b) | No enabled guest login. | Get-LocalUser (SID -501): Enabled | LightDM allow-guest, 'guest' account shell | com.apple.loginwindow GuestEnabled, SMB AllowGuestAccess | — | — |
|
| 74 |
+
| Remote access services restricted | windows, linux, macos | ISO A.8.20; ISO A.6.7; GDPR Art. 32(1)(b) | Only remote access services listed in APPROVED_REMOTE_SERVICES are enabled. | fDenyTSConnections (RDP), sshd/WinRM/TeamViewer/AnyDesk/chromoting services, non-admin SMB shares | ssh/xrdp/vnc/samba/teamviewer/anydesk/chrome-remote-desktop | launchctl print-disabled (sshd, screensharing, smbd, chromoting), ARDAgent/TeamViewer/AnyDesk | — | — |
|
| 75 |
+
| Diagnostic data minimised / Analytics sharing minimised / Usage & diagnostics sharing minimised | windows, linux, macos, ios, android | ISO A.5.34; GDPR Art. 5(1)(c); GDPR Art. 25 | OS diagnostic/usage data sharing is at the minimum level. | DataCollection AllowTelemetry (policy / Settings) | popularity-contest, whoopsie | DiagnosticMessagesHistory AutoSubmit, ThirdPartyDataSubmit | asked: Settings › Privacy & Security › Analytics & Improvements | asked: Settings › Google › All services › Usage & diagnostics (also check the phone maker's diagnostics option) |
|
| 76 |
+
| Notification content hidden on the lock screen / Notification previews hidden when locked / Sensitive notifications hidden when locked | ios, android | ISO A.7.7; ISO A.8.1; GDPR Art. 5(1)(f); GDPR Art. 32(1)(b) | Message and email previews are not shown on the locked screen. | — | — | — | asked: Settings › Notifications › Show Previews | asked: Settings › Notifications › Notifications on lock screen (or Settings › Privacy) |
|
| 77 |
+
| Personal data discovery | windows, linux, macos | ISO A.5.34; ISO A.5.12; GDPR Art. 5(1)(c); GDPR Art. 5(1)(e); GDPR Art. 30 | Opt-in scan of user folders for personal data patterns (counts and paths only). Not implemented in stage 1: NotApplicable. | planned opt-in scan | planned opt-in scan | planned opt-in scan | — | — |
|
| 78 |
|
| 79 |
## EU AI ACT CONTROLS (5)
|
| 80 |
|
| 81 |
+
| Check | Applies to | References | Rule | Windows source | Linux source | macOS source | iPhone source | Android source |
|
| 82 |
+
|---|---|---|---|---|---|---|---|---|
|
| 83 |
+
| AI software inventory | windows, linux, macos, ios, android | ISO A.5.9; GDPR Art. 30; AI Act Art. 26 | AI apps, local AI services and local model folders are listed (Compliant = inventory taken). | uninstall registry + Store apps (AI apps), port 11434, model folders, Claude Code CLI | AI CLIs, snaps, flatpaks, /opt apps, port 11434, model folders | /Applications AI apps, AI CLIs, port 11434, model folders | asked: AI apps installed (checklist); classified by the auditor model | asked: AI apps installed (checklist); classified by the auditor model |
|
| 84 |
+
| Unapproved AI tools (shadow AI) | windows, linux, macos, ios, android | ISO A.5.9; ISO A.8.19; GDPR Art. 28; GDPR Art. 44; AI Act Art. 26 | Every AI tool found is listed in APPROVED_AI_TOOLS. | AI inventory vs APPROVED_AI_TOOLS | AI inventory vs APPROVED_AI_TOOLS | AI inventory vs APPROVED_AI_TOOLS | asked: AI apps installed (checklist); classified by the auditor model | asked: AI apps installed (checklist); classified by the auditor model |
|
| 85 |
+
| AI system risk classification | windows, linux, macos, ios, android | ISO A.5.9; AI Act Art. 5; AI Act Art. 6; AI Act Annex III | Each AI tool found is classified (prohibited / high-risk / limited / minimal). Done by the auditor model: Pending in the collector output, NotApplicable when no AI tool is found. | AI inventory, classified by the model | AI inventory, classified by the model | AI inventory, classified by the model | asked: AI apps installed (checklist); classified by the auditor model | asked: AI apps installed (checklist); classified by the auditor model |
|
| 86 |
+
| Operating system AI features restricted / AI assistant access restricted / Apple Intelligence external integrations restricted / Windows Recall / Copilot data analysis disabled | windows, linux, macos, ios, android | ISO A.5.34; GDPR Art. 5(1)(c); GDPR Art. 25; AI Act Art. 26 | OS-level AI features that capture or send user data are disabled by policy. NotApplicable where the OS has none. Phones: external AI integrations (ChatGPT in Apple Intelligence, assistant access to screen content) are off unless approved. | WindowsAI policies (DisableAIDataAnalysis, AllowRecallEnablement), Recall optional feature | none built in (NotApplicable) | managed com.apple.applicationaccess allowExternalIntelligenceIntegrations (Apple silicon) | asked: Settings › Apple Intelligence & Siri › ChatGPT (only on iPhones that support Apple Intelligence) | asked: Gemini app › your profile picture › Settings › 'Screen context' / 'Use screen context' (other assistants have similar options) |
|
| 87 |
+
| AI usage log retention | windows, linux, macos, ios, android | ISO A.8.15; AI Act Art. 26(6) | Logs kept >= 6 months if a high-risk AI system is used. Pending until classification; NotApplicable when no AI tool is found. | depends on AI classification | depends on AI classification | depends on AI classification | asked: AI apps installed (checklist); classified by the auditor model | asked: AI apps installed (checklist); classified by the auditor model |
|
| 88 |
|
| 89 |
## ORGANISATIONAL CONTROLS (8)
|
| 90 |
|
| 91 |
+
| Check | Applies to | References | Rule | Windows source | Linux source | macOS source | iPhone source | Android source |
|
| 92 |
+
|---|---|---|---|---|---|---|---|---|
|
| 93 |
+
| Record of processing activities | windows, linux, macos, ios, android | ISO A.5.34; GDPR Art. 30 | Answer yes = Compliant, no/partial = Non-Compliant, n/a = NotApplicable, unanswered = Pending. | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation); Pending in a phone check unless passed in the link | organisational.conf (answered once by the organisation); Pending in a phone check unless passed in the link |
|
| 94 |
+
| Data protection impact assessment | windows, linux, macos, ios, android | ISO A.5.34; GDPR Art. 35 | As above. | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation); Pending in a phone check unless passed in the link | organisational.conf (answered once by the organisation); Pending in a phone check unless passed in the link |
|
| 95 |
+
| Breach response procedure | windows, linux, macos, ios, android | ISO A.5.24; ISO A.5.26; GDPR Art. 33; GDPR Art. 34 | As above. | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation); Pending in a phone check unless passed in the link | organisational.conf (answered once by the organisation); Pending in a phone check unless passed in the link |
|
| 96 |
+
| Data processing agreements | windows, linux, macos, ios, android | ISO A.5.20; GDPR Art. 28 | As above. | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation); Pending in a phone check unless passed in the link | organisational.conf (answered once by the organisation); Pending in a phone check unless passed in the link |
|
| 97 |
+
| AI register | windows, linux, macos, ios, android | ISO A.5.9; AI Act Art. 26 | As above. | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation); Pending in a phone check unless passed in the link | organisational.conf (answered once by the organisation); Pending in a phone check unless passed in the link |
|
| 98 |
+
| AI literacy training | windows, linux, macos, ios, android | ISO A.6.3; AI Act Art. 4 | As above. | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation); Pending in a phone check unless passed in the link | organisational.conf (answered once by the organisation); Pending in a phone check unless passed in the link |
|
| 99 |
+
| Human oversight for high-risk AI | windows, linux, macos, ios, android | AI Act Art. 14; AI Act Art. 26 | As above. | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation); Pending in a phone check unless passed in the link | organisational.conf (answered once by the organisation); Pending in a phone check unless passed in the link |
|
| 100 |
+
| AI transparency to users | windows, linux, macos, ios, android | AI Act Art. 50 | As above. | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation) | organisational.conf (answered once by the organisation); Pending in a phone check unless passed in the link | organisational.conf (answered once by the organisation); Pending in a phone check unless passed in the link |
|
| 101 |
|
| 102 |
## Reference titles
|
| 103 |
|
index.html
CHANGED
|
@@ -1,155 +1,91 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
<html lang="en">
|
| 3 |
<head>
|
| 4 |
-
<meta charset="utf-8">
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
-
<
|
| 7 |
-
<meta name="
|
| 8 |
-
<
|
| 9 |
-
|
| 10 |
-
--bg: #f7f8fb; --card: #ffffff; --text: #16181d; --muted: #5b6270; --line: #e3e6ec;
|
| 11 |
-
--red: #ED2939; --blue: #00A1DE; --ok: #1a7f37; --bad: #c4242f; --na: #6e7781; --pending: #9a6700; --err: #8250df;
|
| 12 |
-
--code-bg: #0f1218; --code-text: #e6edf3;
|
| 13 |
-
}
|
| 14 |
-
@media (prefers-color-scheme: dark) {
|
| 15 |
-
:root:not([data-theme="light"]) {
|
| 16 |
-
--bg: #0d1117; --card: #161b22; --text: #e6edf3; --muted: #9da7b3; --line: #2b313a;
|
| 17 |
-
--ok: #3fb950; --bad: #ff6b6b; --na: #8b949e; --pending: #d29922; --err: #b392f0; --code-bg: #0a0d12;
|
| 18 |
-
}
|
| 19 |
-
}
|
| 20 |
-
* { box-sizing: border-box; }
|
| 21 |
-
body { margin: 0; background: var(--bg); color: var(--text); font: 16px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
|
| 22 |
-
main { max-width: 1040px; margin: 0 auto; padding: 32px 16px 64px; }
|
| 23 |
-
.tag { display: inline-block; font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--red); border: 1px solid var(--red); border-radius: 999px; padding: 2px 10px; }
|
| 24 |
-
h1 { font-size: clamp(28px, 5vw, 40px); line-height: 1.15; margin: 14px 0 8px; }
|
| 25 |
-
h2 { font-size: 20px; margin: 36px 0 12px; }
|
| 26 |
-
p.lead { color: var(--muted); font-size: 18px; max-width: 760px; margin: 0; }
|
| 27 |
-
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; margin-top: 20px; }
|
| 28 |
-
.card { background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 16px; }
|
| 29 |
-
.card b { display: block; font-size: 15px; margin-bottom: 4px; }
|
| 30 |
-
.card span { color: var(--muted); font-size: 14px; }
|
| 31 |
-
.tabs { display: flex; gap: 8px; flex-wrap: wrap; margin: 12px 0; }
|
| 32 |
-
.tabs button { font: inherit; font-size: 14px; padding: 6px 14px; border-radius: 999px; border: 1px solid var(--line); background: var(--card); color: var(--text); cursor: pointer; }
|
| 33 |
-
.tabs button[aria-selected="true"] { background: var(--blue); border-color: var(--blue); color: #fff; }
|
| 34 |
-
.counts { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
|
| 35 |
-
.pill { font-size: 13px; font-weight: 600; padding: 3px 10px; border-radius: 999px; background: var(--card); border: 1px solid var(--line); }
|
| 36 |
-
.s-Compliant { color: var(--ok); } .s-Non-Compliant { color: var(--bad); } .s-NotApplicable { color: var(--na); }
|
| 37 |
-
.s-Pending { color: var(--pending); } .s-Error { color: var(--err); }
|
| 38 |
-
.split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr); gap: 12px; }
|
| 39 |
-
@media (max-width: 760px) { .split { grid-template-columns: 1fr; } }
|
| 40 |
-
pre { margin: 0; background: var(--code-bg); color: var(--code-text); border-radius: 12px; padding: 14px; font: 12.5px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; overflow: auto; max-height: 560px; }
|
| 41 |
-
table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
|
| 42 |
-
.table-wrap { background: var(--card); border: 1px solid var(--line); border-radius: 12px; overflow: auto; max-height: 560px; }
|
| 43 |
-
th, td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
| 44 |
-
th { position: sticky; top: 0; background: var(--card); font-size: 12px; text-transform: uppercase; letter-spacing: .03em; color: var(--muted); }
|
| 45 |
-
td.ref { color: var(--muted); font-size: 12.5px; }
|
| 46 |
-
code { font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; background: var(--card); border: 1px solid var(--line); border-radius: 6px; padding: 1px 6px; }
|
| 47 |
-
.note { border-left: 4px solid var(--red); background: var(--card); padding: 12px 16px; border-radius: 0 12px 12px 0; color: var(--muted); }
|
| 48 |
-
ol li { margin: 4px 0; }
|
| 49 |
-
a { color: var(--blue); }
|
| 50 |
-
footer { margin-top: 48px; color: var(--muted); font-size: 14px; }
|
| 51 |
-
</style>
|
| 52 |
</head>
|
| 53 |
<body>
|
| 54 |
-
<
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
<
|
| 58 |
|
| 59 |
-
<
|
| 60 |
-
<
|
| 61 |
-
<
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
<
|
| 73 |
-
|
| 74 |
-
<
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
|
|
|
| 88 |
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
-
|
| 92 |
-
<
|
| 93 |
-
<
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
pad('Audit Date') + ': ' + rec.audit.started_at.replace('T', ' ').slice(0, 19), pad('Audit Mode') + ': ' + rec.audit.mode, line];
|
| 111 |
-
for (const [id, title] of SECTIONS) {
|
| 112 |
-
if (L.blank) out.push('');
|
| 113 |
-
out.push(line, ' ' + (title || L.results), line);
|
| 114 |
-
for (const r of rec.results) if (r.section === id) out.push(r.name, r.status);
|
| 115 |
-
}
|
| 116 |
-
out.push(line, ' ' + L.end, line);
|
| 117 |
-
return out.join('\n');
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
-
function refs(id) {
|
| 121 |
-
const c = catalog && catalog.checks.find(x => x.id === id);
|
| 122 |
-
if (!c) return '';
|
| 123 |
-
return [...c.controls.iso27001.map(x => 'ISO ' + x), ...c.controls.gdpr.map(x => 'GDPR ' + x), ...c.controls.ai_act.map(x => 'AI Act ' + x)].join(' · ');
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
| 127 |
-
|
| 128 |
-
async function show(os) {
|
| 129 |
-
document.querySelectorAll('#tabs button').forEach(b => b.setAttribute('aria-selected', String(b.dataset.os === os)));
|
| 130 |
-
try {
|
| 131 |
-
const rec = await (await fetch(SAMPLES[os])).json();
|
| 132 |
-
document.getElementById('report').textContent = render(rec);
|
| 133 |
-
document.getElementById('counts').innerHTML = Object.entries(rec.summary)
|
| 134 |
-
.map(([k, v]) => `<span class="pill s-${k}">${v} ${k}</span>`).join('');
|
| 135 |
-
document.getElementById('rows').innerHTML = rec.results.map(r =>
|
| 136 |
-
`<tr><td>${esc(r.name)}</td><td class="s-${r.status}"><b>${r.status}</b></td><td>${esc(r.actual)}</td><td class="ref">${esc(refs(r.id))}</td></tr>`).join('');
|
| 137 |
-
} catch (e) {
|
| 138 |
-
document.getElementById('report').textContent = 'Could not load the sample: ' + e.message;
|
| 139 |
-
}
|
| 140 |
-
}
|
| 141 |
-
|
| 142 |
-
(async () => {
|
| 143 |
-
try { catalog = await (await fetch('catalog/checks.json')).json(); } catch (e) { catalog = null; }
|
| 144 |
-
const tabs = document.getElementById('tabs');
|
| 145 |
-
for (const os of Object.keys(SAMPLES)) {
|
| 146 |
-
const b = document.createElement('button');
|
| 147 |
-
b.textContent = LABELS[os]; b.dataset.os = os; b.setAttribute('role', 'tab');
|
| 148 |
-
b.addEventListener('click', () => show(os));
|
| 149 |
-
tabs.appendChild(b);
|
| 150 |
-
}
|
| 151 |
-
show('windows');
|
| 152 |
-
})();
|
| 153 |
-
</script>
|
| 154 |
</body>
|
| 155 |
</html>
|
|
|
|
| 1 |
<!doctype html>
|
| 2 |
<html lang="en">
|
| 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="Free, private security and compliance check for your phone (ISO 27001, GDPR, EU AI Act). Runs in your browser; nothing about your phone is uploaded.">
|
| 7 |
+
<meta name="theme-color" content="#ED2939">
|
| 8 |
+
<title>Devseis Phone Check</title>
|
| 9 |
+
<link rel="stylesheet" href="mobile/styles.css">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
</head>
|
| 11 |
<body>
|
| 12 |
+
<header class="top">
|
| 13 |
+
<div class="brand"><span class="mark">D</span><span>Devseis <b>Phone Check</b></span></div>
|
| 14 |
+
<div id="model-chip" class="chip hidden"></div>
|
| 15 |
+
</header>
|
| 16 |
|
| 17 |
+
<main>
|
| 18 |
+
<!-- 1. Start -->
|
| 19 |
+
<section id="s-start" class="screen">
|
| 20 |
+
<h1>Is this phone ready for work?</h1>
|
| 21 |
+
<p class="lead">A free check against <b>ISO 27001</b>, <b>GDPR</b> and the <b>EU AI Act</b>, written by the Devseis AI auditor <b>inside your browser</b>.</p>
|
| 22 |
+
<div id="device-card" class="card device"><span class="muted">Detecting this device…</span></div>
|
| 23 |
+
<div id="desktop-note" class="card note hidden">
|
| 24 |
+
<p><b>You are on a computer.</b> The Devseis desktop app audits Windows, macOS and Linux automatically (a web page cannot read a computer's settings).</p>
|
| 25 |
+
<p>To try the phone check here, choose a phone type:</p>
|
| 26 |
+
<div class="row"><button class="secondary" data-demo="ios">iPhone</button><button class="secondary" data-demo="android">Android</button></div>
|
| 27 |
+
</div>
|
| 28 |
+
<ul class="facts">
|
| 29 |
+
<li><b>Private:</b> your answers and the report stay on this phone. Only the AI model and vulnerability data are downloaded.</li>
|
| 30 |
+
<li><b>About 5 minutes:</b> the browser detects what it can; you answer the rest, with help on where to look.</li>
|
| 31 |
+
<li><b>Honest:</b> every result says whether it was <i>detected</i> or <i>self-reported</i>.</li>
|
| 32 |
+
</ul>
|
| 33 |
+
<button id="start" class="primary" disabled>Start the check</button>
|
| 34 |
+
<p class="muted center"><a href="about.html">About the project</a></p>
|
| 35 |
+
</section>
|
| 36 |
|
| 37 |
+
<!-- 2. Approval -->
|
| 38 |
+
<section id="s-consent" class="screen hidden">
|
| 39 |
+
<h1>Before we start</h1>
|
| 40 |
+
<div class="card">
|
| 41 |
+
<p><b>What will be checked</b> (<span id="check-count"></span> checks):</p>
|
| 42 |
+
<ul id="plan" class="plan"></ul>
|
| 43 |
+
</div>
|
| 44 |
+
<label class="card toggle">
|
| 45 |
+
<input type="checkbox" id="use-ai">
|
| 46 |
+
<span><b>Use the AI auditor</b><br><span id="ai-note" class="muted"></span></span>
|
| 47 |
+
</label>
|
| 48 |
+
<p class="muted">Nothing on the phone is changed. You can stop at any time.</p>
|
| 49 |
+
<div class="row"><button class="secondary" id="consent-back">Back</button><button class="primary" id="consent-ok">I agree, continue</button></div>
|
| 50 |
+
</section>
|
| 51 |
|
| 52 |
+
<!-- 3. Questions -->
|
| 53 |
+
<section id="s-question" class="screen hidden">
|
| 54 |
+
<div class="progress"><div id="q-bar" class="bar"></div></div>
|
| 55 |
+
<p class="muted" id="q-count"></p>
|
| 56 |
+
<h2 id="q-check" class="check-name"></h2>
|
| 57 |
+
<h1 id="q-text"></h1>
|
| 58 |
+
<div class="where"><span class="where-label">Where to look</span><span id="q-where"></span></div>
|
| 59 |
+
<div id="q-answers" class="answers"></div>
|
| 60 |
+
<div class="row"><button class="secondary" id="q-back">Back</button></div>
|
| 61 |
+
</section>
|
| 62 |
|
| 63 |
+
<!-- 4. Writing the report -->
|
| 64 |
+
<section id="s-work" class="screen hidden">
|
| 65 |
+
<h1 id="w-title">Writing your report</h1>
|
| 66 |
+
<p id="w-step" class="muted"></p>
|
| 67 |
+
<div class="progress big"><div id="w-bar" class="bar"></div></div>
|
| 68 |
+
<p id="w-current" class="muted"></p>
|
| 69 |
+
</section>
|
| 70 |
|
| 71 |
+
<!-- 5. Result -->
|
| 72 |
+
<section id="s-done" class="screen hidden">
|
| 73 |
+
<div id="result"></div>
|
| 74 |
+
<div class="actions">
|
| 75 |
+
<button class="primary" id="share-pdf">Save or share PDF</button>
|
| 76 |
+
<button class="secondary" id="view-report">View full report</button>
|
| 77 |
+
<button class="secondary" id="save-json">Download evidence (JSON)</button>
|
| 78 |
+
</div>
|
| 79 |
+
<p id="pdf-note" class="muted"></p>
|
| 80 |
+
<button class="link" id="again">Check again</button>
|
| 81 |
+
</section>
|
| 82 |
+
</main>
|
| 83 |
|
| 84 |
+
<footer class="foot">
|
| 85 |
+
Open source by Devseis · <a href="about.html">about</a> ·
|
| 86 |
+
<a href="https://huggingface.co/spaces/Devseis/endpoint-auditor/tree/main">code (Apache-2.0)</a> ·
|
| 87 |
+
<a href="https://huggingface.co/datasets/Devseis/endpoint-auditor-synthetic">data (CC BY 4.0)</a> · Not legal advice or a certification.
|
| 88 |
+
</footer>
|
| 89 |
+
<script type="module" src="mobile/app.js"></script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
</body>
|
| 91 |
</html>
|
library/ai_tools.json
CHANGED
|
@@ -1,27 +1,179 @@
|
|
| 1 |
{
|
| 2 |
-
"library_version": "0.
|
| 3 |
"note": "Default classifications assume general office use by a deployer. Under the AI Act the risk class depends on the purpose of use, not on the tool alone. Not legal advice.",
|
| 4 |
"purpose_rules": {
|
| 5 |
-
"prohibited": [
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
"limited_risk": "Chatbots and AI-generated content: people must be told they are dealing with AI, and published deepfakes or AI-written public-interest text must be labelled (Art. 50).",
|
| 8 |
"minimal_risk": "Drafting, summarising, translation, coding help and similar general productivity use."
|
| 9 |
},
|
| 10 |
"tools": {
|
| 11 |
-
"Microsoft Copilot": {
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
"
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
}
|
| 27 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"library_version": "0.3.0",
|
| 3 |
"note": "Default classifications assume general office use by a deployer. Under the AI Act the risk class depends on the purpose of use, not on the tool alone. Not legal advice.",
|
| 4 |
"purpose_rules": {
|
| 5 |
+
"prohibited": [
|
| 6 |
+
"Emotion recognition of employees or students (except for medical or safety reasons)",
|
| 7 |
+
"Social scoring of people",
|
| 8 |
+
"Manipulative or exploitative techniques that cause significant harm",
|
| 9 |
+
"Untargeted scraping of facial images to build face recognition databases",
|
| 10 |
+
"Biometric categorisation inferring sensitive traits such as political views or sexual orientation"
|
| 11 |
+
],
|
| 12 |
+
"high_risk_examples": [
|
| 13 |
+
"Screening or ranking job applicants, or making promotion, task allocation or dismissal decisions",
|
| 14 |
+
"Assessing creditworthiness or credit scores of individuals",
|
| 15 |
+
"Deciding access to education or grading students",
|
| 16 |
+
"Assessing eligibility for public benefits or essential services",
|
| 17 |
+
"Risk assessment and pricing in life and health insurance",
|
| 18 |
+
"Biometric identification of people"
|
| 19 |
+
],
|
| 20 |
"limited_risk": "Chatbots and AI-generated content: people must be told they are dealing with AI, and published deepfakes or AI-written public-interest text must be labelled (Art. 50).",
|
| 21 |
"minimal_risk": "Drafting, summarising, translation, coding help and similar general productivity use."
|
| 22 |
},
|
| 23 |
"tools": {
|
| 24 |
+
"Microsoft Copilot": {
|
| 25 |
+
"kind": "Cloud AI assistant",
|
| 26 |
+
"provider": "Microsoft (US)",
|
| 27 |
+
"data_location": "Microsoft cloud; enterprise plans keep data under Microsoft's commercial data protection terms",
|
| 28 |
+
"default_risk": "minimal",
|
| 29 |
+
"gdpr_notes": "Use the Microsoft 365 work account version covered by the organisation's Microsoft data processing terms; consumer Copilot is not covered."
|
| 30 |
+
},
|
| 31 |
+
"ChatGPT": {
|
| 32 |
+
"kind": "Cloud AI assistant",
|
| 33 |
+
"provider": "OpenAI (US)",
|
| 34 |
+
"data_location": "Provider cloud outside the EEA unless an EU data residency plan is used",
|
| 35 |
+
"default_risk": "minimal",
|
| 36 |
+
"gdpr_notes": "Needs a business plan with a data processing agreement and a transfer basis (Art. 28, Art. 44); consumer accounts may use prompts for training unless opted out."
|
| 37 |
+
},
|
| 38 |
+
"Claude": {
|
| 39 |
+
"kind": "Cloud AI assistant",
|
| 40 |
+
"provider": "Anthropic (US)",
|
| 41 |
+
"data_location": "Provider cloud outside the EEA",
|
| 42 |
+
"default_risk": "minimal",
|
| 43 |
+
"gdpr_notes": "Needs a commercial plan with a data processing agreement and a transfer basis (Art. 28, Art. 44)."
|
| 44 |
+
},
|
| 45 |
+
"Claude Code": {
|
| 46 |
+
"kind": "AI coding assistant (command line)",
|
| 47 |
+
"provider": "Anthropic (US)",
|
| 48 |
+
"data_location": "Code and prompts sent to the provider's cloud",
|
| 49 |
+
"default_risk": "minimal",
|
| 50 |
+
"gdpr_notes": "Source code may contain personal data or secrets; use a commercial plan with a data processing agreement and keep secrets out of repositories."
|
| 51 |
+
},
|
| 52 |
+
"Gemini": {
|
| 53 |
+
"kind": "Cloud AI assistant",
|
| 54 |
+
"provider": "Google (US)",
|
| 55 |
+
"data_location": "Provider cloud",
|
| 56 |
+
"default_risk": "minimal",
|
| 57 |
+
"gdpr_notes": "Use the Workspace version under the organisation's Google data processing terms; consumer use is not covered."
|
| 58 |
+
},
|
| 59 |
+
"Perplexity": {
|
| 60 |
+
"kind": "Cloud AI search assistant",
|
| 61 |
+
"provider": "Perplexity (US)",
|
| 62 |
+
"data_location": "Provider cloud outside the EEA",
|
| 63 |
+
"default_risk": "minimal",
|
| 64 |
+
"gdpr_notes": "Needs an enterprise agreement and a transfer basis before work or personal data is entered."
|
| 65 |
+
},
|
| 66 |
+
"Cursor": {
|
| 67 |
+
"kind": "AI code editor",
|
| 68 |
+
"provider": "Anysphere (US)",
|
| 69 |
+
"data_location": "Code sent to the provider and its model providers unless privacy mode is enforced",
|
| 70 |
+
"default_risk": "minimal",
|
| 71 |
+
"gdpr_notes": "Enforce privacy mode through a business plan and sign a data processing agreement."
|
| 72 |
+
},
|
| 73 |
+
"Windsurf": {
|
| 74 |
+
"kind": "AI code editor",
|
| 75 |
+
"provider": "US-based provider",
|
| 76 |
+
"data_location": "Code sent to the provider's cloud",
|
| 77 |
+
"default_risk": "minimal",
|
| 78 |
+
"gdpr_notes": "Use a business plan with a data processing agreement and zero-retention settings."
|
| 79 |
+
},
|
| 80 |
+
"Ollama": {
|
| 81 |
+
"kind": "Local model runner",
|
| 82 |
+
"provider": "Open source (runs locally)",
|
| 83 |
+
"data_location": "On the device; nothing leaves unless a cloud model is selected",
|
| 84 |
+
"default_risk": "minimal",
|
| 85 |
+
"gdpr_notes": "No transfer to a provider, but check each model's licence and keep the local API (port 11434) closed to the network."
|
| 86 |
+
},
|
| 87 |
+
"LM Studio": {
|
| 88 |
+
"kind": "Local model runner",
|
| 89 |
+
"provider": "Element Labs (runs locally)",
|
| 90 |
+
"data_location": "On the device",
|
| 91 |
+
"default_risk": "minimal",
|
| 92 |
+
"gdpr_notes": "No transfer to a provider; check model licences and whether its local server is exposed."
|
| 93 |
+
},
|
| 94 |
+
"GPT4All": {
|
| 95 |
+
"kind": "Local model runner",
|
| 96 |
+
"provider": "Nomic AI (runs locally)",
|
| 97 |
+
"data_location": "On the device unless optional data sharing is enabled",
|
| 98 |
+
"default_risk": "minimal",
|
| 99 |
+
"gdpr_notes": "Keep the optional data lake sharing switched off."
|
| 100 |
+
},
|
| 101 |
+
"Jan": {
|
| 102 |
+
"kind": "Local model runner",
|
| 103 |
+
"provider": "Open source (runs locally)",
|
| 104 |
+
"data_location": "On the device unless a cloud model is connected",
|
| 105 |
+
"default_risk": "minimal",
|
| 106 |
+
"gdpr_notes": "Check whether remote API providers are configured; if so, the cloud provider rules apply."
|
| 107 |
+
},
|
| 108 |
+
"Msty": {
|
| 109 |
+
"kind": "Desktop AI client",
|
| 110 |
+
"provider": "Desktop app (local and cloud models)",
|
| 111 |
+
"data_location": "Local, or the cloud provider of any connected model",
|
| 112 |
+
"default_risk": "minimal",
|
| 113 |
+
"gdpr_notes": "Treat connected cloud models like the corresponding cloud assistant."
|
| 114 |
+
},
|
| 115 |
+
"Alpaca (Ollama client)": {
|
| 116 |
+
"kind": "Desktop client for local models",
|
| 117 |
+
"provider": "Open source (runs locally)",
|
| 118 |
+
"data_location": "On the device",
|
| 119 |
+
"default_risk": "minimal",
|
| 120 |
+
"gdpr_notes": "No transfer to a provider; check model licences."
|
| 121 |
+
},
|
| 122 |
+
"Hugging Face models": {
|
| 123 |
+
"kind": "Local model files",
|
| 124 |
+
"provider": "Various model publishers",
|
| 125 |
+
"data_location": "On the device",
|
| 126 |
+
"default_risk": "minimal",
|
| 127 |
+
"gdpr_notes": "Record which models are present and their licences; the risk class depends on what the models are used for."
|
| 128 |
+
},
|
| 129 |
+
"Meta AI": {
|
| 130 |
+
"kind": "Cloud AI assistant (built into WhatsApp, Instagram and Facebook apps)",
|
| 131 |
+
"provider": "Meta (US)",
|
| 132 |
+
"data_location": "Provider cloud outside the EEA",
|
| 133 |
+
"default_risk": "minimal",
|
| 134 |
+
"gdpr_notes": "Consumer service with no business data processing terms; do not share work or customer data in chats with Meta AI."
|
| 135 |
+
},
|
| 136 |
+
"Grok": {
|
| 137 |
+
"kind": "Cloud AI assistant",
|
| 138 |
+
"provider": "xAI (US)",
|
| 139 |
+
"data_location": "Provider cloud outside the EEA",
|
| 140 |
+
"default_risk": "minimal",
|
| 141 |
+
"gdpr_notes": "Consumer service; needs an enterprise agreement and a transfer basis before work or personal data is entered."
|
| 142 |
+
},
|
| 143 |
+
"DeepSeek": {
|
| 144 |
+
"kind": "Cloud AI assistant",
|
| 145 |
+
"provider": "DeepSeek (China)",
|
| 146 |
+
"data_location": "Provider cloud in the People's Republic of China",
|
| 147 |
+
"default_risk": "minimal",
|
| 148 |
+
"gdpr_notes": "No EU adequacy decision for China and no business data processing terms; EU data protection authorities have restricted or investigated it. Do not use with work or personal data."
|
| 149 |
+
},
|
| 150 |
+
"Character.AI": {
|
| 151 |
+
"kind": "AI companion chatbot",
|
| 152 |
+
"provider": "Character Technologies (US)",
|
| 153 |
+
"data_location": "Provider cloud outside the EEA",
|
| 154 |
+
"default_risk": "limited",
|
| 155 |
+
"gdpr_notes": "Consumer entertainment chatbot; not suitable for work use. Users must be told they are talking to an AI (AI Act Art. 50)."
|
| 156 |
+
},
|
| 157 |
+
"Replika": {
|
| 158 |
+
"kind": "AI companion chatbot",
|
| 159 |
+
"provider": "Luka (US)",
|
| 160 |
+
"data_location": "Provider cloud outside the EEA",
|
| 161 |
+
"default_risk": "limited",
|
| 162 |
+
"gdpr_notes": "Consumer companion app that collects sensitive personal conversations and has been subject to EU data protection enforcement; not suitable for work use."
|
| 163 |
+
},
|
| 164 |
+
"Otter": {
|
| 165 |
+
"kind": "AI meeting transcription",
|
| 166 |
+
"provider": "Otter.ai (US)",
|
| 167 |
+
"data_location": "Provider cloud outside the EEA",
|
| 168 |
+
"default_risk": "minimal",
|
| 169 |
+
"gdpr_notes": "Records and transcribes other people's voices: needs a lawful basis and notice to meeting participants, plus a data processing agreement."
|
| 170 |
+
},
|
| 171 |
+
"Grammarly": {
|
| 172 |
+
"kind": "AI writing assistant (keyboard and browser)",
|
| 173 |
+
"provider": "Grammarly (US)",
|
| 174 |
+
"data_location": "Provider cloud outside the EEA",
|
| 175 |
+
"default_risk": "minimal",
|
| 176 |
+
"gdpr_notes": "Reads everything typed where it is enabled, including personal data; use the business plan with a data processing agreement or disable it in work apps."
|
| 177 |
+
}
|
| 178 |
}
|
| 179 |
}
|
library/check_guidance.json
CHANGED
|
@@ -1,10 +1,16 @@
|
|
| 1 |
{
|
| 2 |
-
"library_version": "0.
|
| 3 |
"note": "Per-check guidance used to write findings. Fix steps are written for administrators; prefer central management (Group Policy/Intune, MDM, configuration management) over per-device changes. Review by a security professional before use with customers.",
|
| 4 |
"aliases": {
|
| 5 |
-
"firewall.domain.enabled": "firewall.enabled",
|
| 6 |
-
"firewall.
|
| 7 |
-
"firewall.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
"encryption.feature": "encryption.enabled"
|
| 9 |
},
|
| 10 |
"status_notes": {
|
|
@@ -15,537 +21,1204 @@
|
|
| 15 |
"checks": {
|
| 16 |
"password.min_length": {
|
| 17 |
"why": "Short passwords can be guessed or cracked quickly; length is the strongest single factor in password strength.",
|
| 18 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 19 |
"fix": {
|
| 20 |
-
"windows": [
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
},
|
| 24 |
"verify": "Re-run the audit; Minimum password length should read {PASSWORD_MIN_LENGTH} or more."
|
| 25 |
},
|
| 26 |
"password.history": {
|
| 27 |
"why": "Without a password history, users can return to old, possibly exposed passwords.",
|
| 28 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 29 |
"fix": {
|
| 30 |
-
"windows": [
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
},
|
| 34 |
"verify": "Re-run the audit; Password history should read {PASSWORD_HISTORY} or more."
|
| 35 |
},
|
| 36 |
"password.expiration": {
|
| 37 |
"why": "The baseline requires passwords to be changed within a set period, limiting how long a stolen password stays useful.",
|
| 38 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 39 |
"fix": {
|
| 40 |
-
"windows": [
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
},
|
| 44 |
"verify": "Re-run the audit; Password expiration should be between 1 and {PASSWORD_MAX_AGE_DAYS} days.",
|
| 45 |
"note": "Current NIST guidance (SP 800-63B) advises against forced periodic changes when passwords are long and checked against breach lists. Set PASSWORD_MAX_AGE_DAYS=0 in baseline.conf if your policy follows that approach."
|
| 46 |
},
|
| 47 |
"password.min_age": {
|
| 48 |
"why": "A minimum age stops users cycling through passwords immediately to get back to an old one.",
|
| 49 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 50 |
"fix": {
|
| 51 |
-
"windows": [
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
},
|
| 55 |
"verify": "Re-run the audit; Minimum password age should read {PASSWORD_MIN_AGE_DAYS} day(s) or more."
|
| 56 |
},
|
| 57 |
"password.complexity": {
|
| 58 |
"why": "Mixing character types makes passwords harder to guess.",
|
| 59 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 60 |
"fix": {
|
| 61 |
-
"windows": [
|
| 62 |
-
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
},
|
| 65 |
"verify": "Re-run the audit; Password complexity should be Compliant."
|
| 66 |
},
|
| 67 |
"lockout.threshold": {
|
| 68 |
"why": "Locking an account after a few wrong passwords stops online guessing.",
|
| 69 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 70 |
"fix": {
|
| 71 |
-
"windows": [
|
| 72 |
-
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
},
|
| 75 |
"verify": "Re-run the audit; Account lockout threshold should be between 1 and {LOCKOUT_THRESHOLD_MAX}."
|
| 76 |
},
|
| 77 |
"lockout.duration": {
|
| 78 |
"why": "The lockout must last long enough to slow down repeated guessing.",
|
| 79 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 80 |
"fix": {
|
| 81 |
-
"windows": [
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
},
|
| 85 |
"verify": "Re-run the audit; Account lockout duration should be {LOCKOUT_DURATION_MIN_MINUTES} minutes or more."
|
| 86 |
},
|
| 87 |
"lockout.observation": {
|
| 88 |
"why": "Failed attempts must be counted over a long enough window to detect slow guessing.",
|
| 89 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 90 |
"fix": {
|
| 91 |
-
"windows": [
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
},
|
| 95 |
"verify": "Re-run the audit; Lockout observation period should be {LOCKOUT_OBSERVATION_MIN_MINUTES} minutes or more."
|
| 96 |
},
|
| 97 |
"malware.antivirus": {
|
| 98 |
"why": "Anti-malware software detects and blocks known malicious files and behaviour.",
|
| 99 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 100 |
"fix": {
|
| 101 |
-
"windows": [
|
| 102 |
-
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
},
|
| 105 |
"verify": "Re-run the audit; Antivirus should be Compliant."
|
| 106 |
},
|
| 107 |
"malware.service": {
|
| 108 |
"why": "The malware protection service must be running for protection to work.",
|
| 109 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 110 |
"fix": {
|
| 111 |
-
"windows": [
|
| 112 |
-
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
},
|
| 115 |
"verify": "Re-run the audit; the malware protection service should be Compliant."
|
| 116 |
},
|
| 117 |
"malware.realtime": {
|
| 118 |
"why": "Real-time protection scans files and processes as they are used.",
|
| 119 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 120 |
"fix": {
|
| 121 |
-
"windows": [
|
|
|
|
|
|
|
| 122 |
},
|
| 123 |
"verify": "Re-run the audit; Real-time protection should be on."
|
| 124 |
},
|
| 125 |
"malware.on_access": {
|
| 126 |
"why": "On-access scanning checks files when they are opened or written.",
|
| 127 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 128 |
"fix": {
|
| 129 |
-
"windows": [
|
| 130 |
-
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
},
|
| 133 |
"verify": "Re-run the audit; On-access malware protection should be Compliant."
|
| 134 |
},
|
| 135 |
"malware.tamper": {
|
| 136 |
"why": "Tamper protection stops malware or users switching off Defender.",
|
| 137 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 138 |
"fix": {
|
| 139 |
-
"windows": [
|
|
|
|
|
|
|
| 140 |
},
|
| 141 |
"verify": "Re-run the audit; Tamper protection should be on."
|
| 142 |
},
|
| 143 |
"malware.signatures": {
|
| 144 |
"why": "Anti-malware can only detect threats it knows about; signatures must be current.",
|
| 145 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 146 |
"fix": {
|
| 147 |
-
"windows": [
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
},
|
| 151 |
"verify": "Re-run the audit; signatures should be within the allowed age."
|
| 152 |
},
|
| 153 |
"malware.signature_updates": {
|
| 154 |
"why": "Signature updates must happen automatically so protection stays current without user action.",
|
| 155 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 156 |
"fix": {
|
| 157 |
-
"linux": [
|
| 158 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
},
|
| 160 |
"verify": "Re-run the audit; signature updates should be automatic."
|
| 161 |
},
|
| 162 |
"firewall.enabled": {
|
| 163 |
"why": "A host firewall blocks unwanted network connections to the device, especially on public networks.",
|
| 164 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 165 |
"fix": {
|
| 166 |
-
"windows": [
|
| 167 |
-
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
},
|
| 170 |
"verify": "Re-run the audit; the firewall should be on."
|
| 171 |
},
|
| 172 |
"firewall.inbound": {
|
| 173 |
"why": "Unsolicited inbound connections should be blocked unless a service is explicitly allowed.",
|
| 174 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 175 |
"fix": {
|
| 176 |
-
"windows": [
|
| 177 |
-
|
| 178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
},
|
| 180 |
"verify": "Re-run the audit; the inbound policy should be block/deny."
|
| 181 |
},
|
| 182 |
"firewall.outbound": {
|
| 183 |
"why": "The baseline fixes the default outbound policy so that it is consistent across devices.",
|
| 184 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 185 |
"fix": {
|
| 186 |
-
"windows": [
|
| 187 |
-
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
},
|
| 190 |
"verify": "Re-run the audit; the outbound policy should read {FIREWALL_OUTBOUND}."
|
| 191 |
},
|
| 192 |
"screen_lock.enabled": {
|
| 193 |
"why": "An idle device should lock itself so nobody can use it while the owner is away.",
|
| 194 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 195 |
"fix": {
|
| 196 |
-
"windows": [
|
| 197 |
-
|
| 198 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
},
|
| 200 |
"verify": "Re-run the audit; Screen lock should be on."
|
| 201 |
},
|
| 202 |
"screen_lock.timeout": {
|
| 203 |
"why": "The shorter the idle time before locking, the smaller the window for misuse.",
|
| 204 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 205 |
"fix": {
|
| 206 |
-
"windows": [
|
| 207 |
-
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
},
|
| 210 |
"verify": "Re-run the audit; Screen lock timeout should be {SCREEN_LOCK_MAX_SECONDS} seconds or less."
|
| 211 |
},
|
| 212 |
"screen_lock.password": {
|
| 213 |
"why": "Unlocking the screen must require the user's password.",
|
| 214 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 215 |
"fix": {
|
| 216 |
-
"windows": [
|
| 217 |
-
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
},
|
| 220 |
"verify": "Re-run the audit; the password requirement should be Compliant."
|
| 221 |
},
|
| 222 |
"usb.read": {
|
| 223 |
"why": "The baseline sets whether USB storage may be read; reading lets malware and unapproved data enter.",
|
| 224 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 225 |
"fix": {
|
| 226 |
-
"windows": [
|
| 227 |
-
|
| 228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
},
|
| 230 |
"verify": "Re-run the audit; USB storage read should read {USB_READ}."
|
| 231 |
},
|
| 232 |
"usb.write": {
|
| 233 |
"why": "Writing to USB drives is a common way for data to leave the organisation, often unencrypted.",
|
| 234 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 235 |
"fix": {
|
| 236 |
-
"windows": [
|
| 237 |
-
|
| 238 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
},
|
| 240 |
"verify": "Re-run the audit; USB storage write should read {USB_WRITE}."
|
| 241 |
},
|
| 242 |
"admin.approved": {
|
| 243 |
"why": "A known, approved IT account should manage the device so that support does not depend on users' own admin rights.",
|
| 244 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 245 |
"fix": {
|
| 246 |
-
"windows": [
|
| 247 |
-
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
},
|
| 250 |
"verify": "Re-run the audit; Approved IT administrator should list the approved account."
|
| 251 |
},
|
| 252 |
"admin.unauthorized": {
|
| 253 |
"why": "Everyday user accounts with administrator rights let malware and mistakes change anything on the device.",
|
| 254 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 255 |
"fix": {
|
| 256 |
-
"windows": [
|
| 257 |
-
|
| 258 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
},
|
| 260 |
"verify": "Re-run the audit; Unauthorized administrator accounts should read none."
|
| 261 |
},
|
| 262 |
"encryption.enabled": {
|
| 263 |
"why": "Full-disk encryption keeps data unreadable if the device is lost or stolen.",
|
| 264 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 265 |
"fix": {
|
| 266 |
-
"windows": [
|
| 267 |
-
|
| 268 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
},
|
| 270 |
"verify": "Re-run the audit; Encryption should be Compliant."
|
| 271 |
},
|
| 272 |
"encryption.protection": {
|
| 273 |
"why": "Encryption only protects the data when it is active, fully applied and uses a strong cipher.",
|
| 274 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 275 |
"fix": {
|
| 276 |
-
"windows": [
|
| 277 |
-
|
| 278 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
},
|
| 280 |
"verify": "Re-run the audit; Disk encryption protection should be Compliant."
|
| 281 |
},
|
| 282 |
"encryption.recovery": {
|
| 283 |
"why": "A recovery key lets IT unlock the disk if the user forgets the password or the hardware changes.",
|
| 284 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 285 |
"fix": {
|
| 286 |
-
"windows": [
|
| 287 |
-
|
| 288 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
},
|
| 290 |
"verify": "Re-run the audit; Disk encryption recovery should be Compliant."
|
| 291 |
},
|
| 292 |
"usb.device_access": {
|
| 293 |
"why": "Blocking USB mass storage devices completely is the strongest protection against data leaving on, or malware arriving from, removable drives.",
|
| 294 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 295 |
"fix": {
|
| 296 |
-
"windows": [
|
| 297 |
-
|
| 298 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
},
|
| 300 |
"verify": "Re-run the audit; USB storage device access should read {USB_DEVICE_ACCESS}."
|
| 301 |
},
|
| 302 |
"user.account_type": {
|
| 303 |
"why": "Day-to-day work under an administrator account lets any malware or mistake change the whole system.",
|
| 304 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 305 |
"fix": {
|
| 306 |
-
"windows": [
|
| 307 |
-
|
| 308 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
},
|
| 310 |
"verify": "Re-run the audit; Signed-in user account type should read Standard."
|
| 311 |
},
|
| 312 |
"vuln.pending_updates": {
|
| 313 |
"why": "Security updates that are offered but not installed leave known, published vulnerabilities open.",
|
| 314 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 315 |
"fix": {
|
| 316 |
-
"windows": [
|
| 317 |
-
|
| 318 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
},
|
| 320 |
"verify": "Re-run the audit; pending security updates should be {MAX_PENDING_SECURITY_UPDATES} or fewer."
|
| 321 |
},
|
| 322 |
"vuln.outdated_apps": {
|
| 323 |
"why": "Browsers, office suites, PDF readers and meeting apps are common attack targets and need updates like the OS.",
|
| 324 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 325 |
"fix": {
|
| 326 |
-
"windows": [
|
| 327 |
-
|
| 328 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
},
|
| 330 |
"verify": "Re-run the audit; outdated applications should be {MAX_OUTDATED_APPS} or fewer."
|
| 331 |
},
|
| 332 |
"vuln.known_vulnerabilities": {
|
| 333 |
"why": "Installed software versions with published critical or high vulnerabilities give attackers a ready-made way in.",
|
| 334 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 335 |
"fix": {
|
| 336 |
-
"all": [
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
},
|
| 341 |
"verify": "Re-run the audit; critical or high vulnerabilities should be {MAX_CRITICAL_HIGH_VULNERABILITIES} or fewer."
|
| 342 |
},
|
| 343 |
-
|
| 344 |
"gdpr.auto_updates": {
|
| 345 |
"why": "Security fixes must be installed automatically so that known vulnerabilities are closed quickly.",
|
| 346 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 347 |
"fix": {
|
| 348 |
-
"windows": [
|
| 349 |
-
|
| 350 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
},
|
| 352 |
"verify": "Re-run the audit; Automatic security updates should be Compliant."
|
| 353 |
},
|
| 354 |
"gdpr.updates_recent": {
|
| 355 |
"why": "Recent installs show that patching is actually happening, not only configured.",
|
| 356 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 357 |
"fix": {
|
| 358 |
-
"windows": [
|
| 359 |
-
|
| 360 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
},
|
| 362 |
"verify": "Re-run the audit; the last update should be within {UPDATES_MAX_AGE_DAYS} days."
|
| 363 |
},
|
| 364 |
"gdpr.os_supported": {
|
| 365 |
"why": "An operating system that no longer receives security updates cannot be kept secure.",
|
| 366 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 367 |
"fix": {
|
| 368 |
-
"windows": [
|
| 369 |
-
|
| 370 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
},
|
| 372 |
"verify": "Re-run the audit; Supported operating system version should be Compliant."
|
| 373 |
},
|
| 374 |
"gdpr.security_logging": {
|
| 375 |
"why": "Logon and account-change events are needed to detect and investigate a breach within the 72-hour GDPR deadline.",
|
| 376 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 377 |
"fix": {
|
| 378 |
-
"windows": [
|
| 379 |
-
|
| 380 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
},
|
| 382 |
"verify": "Re-run the audit; Security event logging should be Compliant."
|
| 383 |
},
|
| 384 |
"gdpr.time_sync": {
|
| 385 |
"why": "Accurate clocks are needed to put events in the right order during an investigation.",
|
| 386 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 387 |
"fix": {
|
| 388 |
-
"windows": [
|
| 389 |
-
|
| 390 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
},
|
| 392 |
"verify": "Re-run the audit; Time synchronisation should be Compliant."
|
| 393 |
},
|
| 394 |
"gdpr.backup_configured": {
|
| 395 |
"why": "Backups make it possible to restore personal data after loss, theft, failure or ransomware.",
|
| 396 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 397 |
"fix": {
|
| 398 |
-
"windows": [
|
| 399 |
-
|
| 400 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
},
|
| 402 |
"verify": "Re-run the audit; Backup configured should be Compliant."
|
| 403 |
},
|
| 404 |
"gdpr.backup_recent": {
|
| 405 |
"why": "A configured backup only helps if it has actually run recently.",
|
| 406 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 407 |
"fix": {
|
| 408 |
-
"windows": [
|
| 409 |
-
|
| 410 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
},
|
| 412 |
"verify": "Re-run the audit; the last backup should be within {BACKUP_MAX_AGE_DAYS} days."
|
| 413 |
},
|
| 414 |
"gdpr.removable_encryption": {
|
| 415 |
"why": "Personal data copied to removable media must be encrypted, or copying must be blocked.",
|
| 416 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 417 |
"fix": {
|
| 418 |
-
"windows": [
|
| 419 |
-
|
| 420 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
},
|
| 422 |
"verify": "Re-run the audit; Removable media encryption should be Compliant."
|
| 423 |
},
|
| 424 |
"gdpr.guest_account": {
|
| 425 |
"why": "Guest logins let people use the device without an identifiable account.",
|
| 426 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 427 |
"fix": {
|
| 428 |
-
"windows": [
|
| 429 |
-
|
| 430 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
},
|
| 432 |
"verify": "Re-run the audit; Guest account disabled should be Compliant."
|
| 433 |
},
|
| 434 |
"gdpr.remote_access": {
|
| 435 |
"why": "Remote access services let others reach the device over the network; only approved ones should be on.",
|
| 436 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 437 |
"fix": {
|
| 438 |
-
"windows": [
|
| 439 |
-
|
| 440 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
},
|
| 442 |
"verify": "Re-run the audit; only approved services ({APPROVED_REMOTE_SERVICES_TEXT}) should remain.",
|
| 443 |
"note": "If a listed service is needed, add it to APPROVED_REMOTE_SERVICES in baseline.conf and secure it (multi-factor authentication, restricted to the VPN)."
|
| 444 |
},
|
| 445 |
"gdpr.telemetry": {
|
| 446 |
"why": "Operating systems can send optional usage and diagnostic data, which may contain personal data, to the vendor.",
|
| 447 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 448 |
"fix": {
|
| 449 |
-
"windows": [
|
| 450 |
-
|
| 451 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
},
|
| 453 |
"verify": "Re-run the audit; Diagnostic data minimised should be Compliant."
|
| 454 |
},
|
| 455 |
"gdpr.personal_data_discovery": {
|
| 456 |
"why": "Knowing where personal data is stored on endpoints is needed for the record of processing and for storage limitation.",
|
| 457 |
-
"risk": {
|
| 458 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 459 |
"verify": "Not applicable in stage 1."
|
| 460 |
},
|
| 461 |
-
|
| 462 |
"aiact.inventory": {
|
| 463 |
"why": "Deployers must know which AI systems they use before they can classify and govern them.",
|
| 464 |
-
"risk": {
|
| 465 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 466 |
"verify": "The AI register lists every tool in the inventory."
|
| 467 |
},
|
| 468 |
"aiact.unapproved": {
|
| 469 |
"why": "AI tools used without approval may send work and personal data to providers without contracts or a transfer basis.",
|
| 470 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 471 |
"fix": {
|
| 472 |
-
"all": [
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 476 |
},
|
| 477 |
"verify": "Re-run the audit; Unapproved AI tools should read none."
|
| 478 |
},
|
| 479 |
"aiact.risk_classification": {
|
| 480 |
"why": "Each AI system must be checked against the banned practices and the high-risk uses in Annex III.",
|
| 481 |
-
"risk": {
|
| 482 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
"verify": "Every AI tool in the inventory has a classification in the AI register."
|
| 484 |
},
|
| 485 |
"aiact.os_ai_features": {
|
| 486 |
"why": "Built-in OS AI features can capture screen content or send text to external AI services.",
|
| 487 |
-
"risk": {
|
|
|
|
|
|
|
|
|
|
| 488 |
"fix": {
|
| 489 |
-
"windows": [
|
| 490 |
-
|
| 491 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
},
|
| 493 |
"verify": "Re-run the audit; the OS AI features check should be Compliant or NotApplicable."
|
| 494 |
},
|
| 495 |
"aiact.log_retention": {
|
| 496 |
"why": "Deployers of high-risk AI systems must keep the system's logs for at least six months.",
|
| 497 |
-
"risk": {
|
| 498 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 499 |
"verify": "Log retention is documented for each high-risk AI system."
|
| 500 |
},
|
| 501 |
-
|
| 502 |
"org.ropa": {
|
| 503 |
"why": "The record of processing activities is the core GDPR accountability document.",
|
| 504 |
-
"risk": {
|
| 505 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 506 |
"verify": "Answer ORG_ROPA=yes in organisational.conf once the record is current."
|
| 507 |
},
|
| 508 |
"org.dpia": {
|
| 509 |
"why": "High-risk processing needs a data protection impact assessment before it starts.",
|
| 510 |
-
"risk": {
|
| 511 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 512 |
"verify": "Answer ORG_DPIA=yes once DPIAs are done."
|
| 513 |
},
|
| 514 |
"org.breach_procedure": {
|
| 515 |
"why": "Breaches must be reported to the supervisory authority within 72 hours where required.",
|
| 516 |
-
"risk": {
|
| 517 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 518 |
"verify": "Answer ORG_BREACH_PROCEDURE=yes once the procedure is written and tested."
|
| 519 |
},
|
| 520 |
"org.processor_agreements": {
|
| 521 |
"why": "Every processor, including cloud and AI providers, needs a contract with the GDPR Article 28 terms.",
|
| 522 |
-
"risk": {
|
| 523 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
"verify": "Answer ORG_PROCESSOR_AGREEMENTS=yes once all agreements are in place."
|
| 525 |
},
|
| 526 |
"org.ai_register": {
|
| 527 |
"why": "A register of AI systems is the basis for AI Act classification and deployer duties.",
|
| 528 |
-
"risk": {
|
| 529 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 530 |
"verify": "Answer ORG_AI_REGISTER=yes once the register exists."
|
| 531 |
},
|
| 532 |
"org.ai_literacy": {
|
| 533 |
"why": "Since 2 February 2025, organisations must ensure staff using AI have sufficient AI literacy.",
|
| 534 |
-
"risk": {
|
| 535 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 536 |
"verify": "Answer ORG_AI_LITERACY=yes once training is delivered and recorded."
|
| 537 |
},
|
| 538 |
"org.human_oversight": {
|
| 539 |
"why": "High-risk AI systems need trained people who can understand, monitor and override them.",
|
| 540 |
-
"risk": {
|
| 541 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 542 |
"verify": "Answer ORG_HUMAN_OVERSIGHT=yes (or n/a) in organisational.conf."
|
| 543 |
},
|
| 544 |
"org.ai_transparency": {
|
| 545 |
"why": "People must know when they deal with AI or AI-generated content.",
|
| 546 |
-
"risk": {
|
| 547 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
"verify": "Answer ORG_AI_TRANSPARENCY=yes once the rules are in place."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
}
|
| 550 |
}
|
| 551 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"library_version": "0.3.0",
|
| 3 |
"note": "Per-check guidance used to write findings. Fix steps are written for administrators; prefer central management (Group Policy/Intune, MDM, configuration management) over per-device changes. Review by a security professional before use with customers.",
|
| 4 |
"aliases": {
|
| 5 |
+
"firewall.domain.enabled": "firewall.enabled",
|
| 6 |
+
"firewall.private.enabled": "firewall.enabled",
|
| 7 |
+
"firewall.public.enabled": "firewall.enabled",
|
| 8 |
+
"firewall.domain.inbound": "firewall.inbound",
|
| 9 |
+
"firewall.private.inbound": "firewall.inbound",
|
| 10 |
+
"firewall.public.inbound": "firewall.inbound",
|
| 11 |
+
"firewall.domain.outbound": "firewall.outbound",
|
| 12 |
+
"firewall.private.outbound": "firewall.outbound",
|
| 13 |
+
"firewall.public.outbound": "firewall.outbound",
|
| 14 |
"encryption.feature": "encryption.enabled"
|
| 15 |
},
|
| 16 |
"status_notes": {
|
|
|
|
| 21 |
"checks": {
|
| 22 |
"password.min_length": {
|
| 23 |
"why": "Short passwords can be guessed or cracked quickly; length is the strongest single factor in password strength.",
|
| 24 |
+
"risk": {
|
| 25 |
+
"level": "Medium",
|
| 26 |
+
"reason": "Weak passwords make account takeover through guessing or credential stuffing easier."
|
| 27 |
+
},
|
| 28 |
"fix": {
|
| 29 |
+
"windows": [
|
| 30 |
+
"Set Group Policy: Computer Configuration > Windows Settings > Security Settings > Account Policies > Password Policy > Minimum password length = {PASSWORD_MIN_LENGTH} (or the Intune equivalent).",
|
| 31 |
+
"For a stand-alone device: net accounts /minpwlen:{PASSWORD_MIN_LENGTH}"
|
| 32 |
+
],
|
| 33 |
+
"linux": [
|
| 34 |
+
"Install pam_pwquality (Debian/Ubuntu: apt install libpam-pwquality) and make sure it is listed in the PAM password stack.",
|
| 35 |
+
"Set minlen = {PASSWORD_MIN_LENGTH} in /etc/security/pwquality.conf."
|
| 36 |
+
],
|
| 37 |
+
"macos": [
|
| 38 |
+
"Deploy a Passcode configuration profile through your MDM with minLength = {PASSWORD_MIN_LENGTH}."
|
| 39 |
+
]
|
| 40 |
},
|
| 41 |
"verify": "Re-run the audit; Minimum password length should read {PASSWORD_MIN_LENGTH} or more."
|
| 42 |
},
|
| 43 |
"password.history": {
|
| 44 |
"why": "Without a password history, users can return to old, possibly exposed passwords.",
|
| 45 |
+
"risk": {
|
| 46 |
+
"level": "Low",
|
| 47 |
+
"reason": "Reused passwords keep compromised credentials valid."
|
| 48 |
+
},
|
| 49 |
"fix": {
|
| 50 |
+
"windows": [
|
| 51 |
+
"Set Group Policy: Account Policies > Password Policy > Enforce password history = {PASSWORD_HISTORY}.",
|
| 52 |
+
"For a stand-alone device: net accounts /uniquepw:{PASSWORD_HISTORY}"
|
| 53 |
+
],
|
| 54 |
+
"linux": [
|
| 55 |
+
"Add pam_pwhistory to the PAM password stack before pam_unix (e.g. 'password requisite pam_pwhistory.so remember={PASSWORD_HISTORY} use_authtok').",
|
| 56 |
+
"Or set remember = {PASSWORD_HISTORY} in /etc/security/pwhistory.conf where supported."
|
| 57 |
+
],
|
| 58 |
+
"macos": [
|
| 59 |
+
"Deploy a Passcode configuration profile with pinHistory = {PASSWORD_HISTORY}."
|
| 60 |
+
]
|
| 61 |
},
|
| 62 |
"verify": "Re-run the audit; Password history should read {PASSWORD_HISTORY} or more."
|
| 63 |
},
|
| 64 |
"password.expiration": {
|
| 65 |
"why": "The baseline requires passwords to be changed within a set period, limiting how long a stolen password stays useful.",
|
| 66 |
+
"risk": {
|
| 67 |
+
"level": "Low",
|
| 68 |
+
"reason": "A leaked password can stay valid indefinitely."
|
| 69 |
+
},
|
| 70 |
"fix": {
|
| 71 |
+
"windows": [
|
| 72 |
+
"Set Group Policy: Account Policies > Password Policy > Maximum password age = {PASSWORD_MAX_AGE_DAYS} days.",
|
| 73 |
+
"For a stand-alone device: net accounts /maxpwage:{PASSWORD_MAX_AGE_DAYS}"
|
| 74 |
+
],
|
| 75 |
+
"linux": [
|
| 76 |
+
"Set PASS_MAX_DAYS {PASSWORD_MAX_AGE_DAYS} in /etc/login.defs (applies to new accounts).",
|
| 77 |
+
"Apply it to existing accounts: chage --maxdays {PASSWORD_MAX_AGE_DAYS} <user>"
|
| 78 |
+
],
|
| 79 |
+
"macos": [
|
| 80 |
+
"Deploy a Passcode configuration profile with maxPINAgeInDays = {PASSWORD_MAX_AGE_DAYS}."
|
| 81 |
+
]
|
| 82 |
},
|
| 83 |
"verify": "Re-run the audit; Password expiration should be between 1 and {PASSWORD_MAX_AGE_DAYS} days.",
|
| 84 |
"note": "Current NIST guidance (SP 800-63B) advises against forced periodic changes when passwords are long and checked against breach lists. Set PASSWORD_MAX_AGE_DAYS=0 in baseline.conf if your policy follows that approach."
|
| 85 |
},
|
| 86 |
"password.min_age": {
|
| 87 |
"why": "A minimum age stops users cycling through passwords immediately to get back to an old one.",
|
| 88 |
+
"risk": {
|
| 89 |
+
"level": "Low",
|
| 90 |
+
"reason": "Without it, the password history rule can be bypassed."
|
| 91 |
+
},
|
| 92 |
"fix": {
|
| 93 |
+
"windows": [
|
| 94 |
+
"Set Group Policy: Account Policies > Password Policy > Minimum password age = {PASSWORD_MIN_AGE_DAYS} day(s).",
|
| 95 |
+
"For a stand-alone device: net accounts /minpwage:{PASSWORD_MIN_AGE_DAYS}"
|
| 96 |
+
],
|
| 97 |
+
"linux": [
|
| 98 |
+
"Set PASS_MIN_DAYS {PASSWORD_MIN_AGE_DAYS} in /etc/login.defs and apply to existing accounts: chage --mindays {PASSWORD_MIN_AGE_DAYS} <user>"
|
| 99 |
+
],
|
| 100 |
+
"macos": [
|
| 101 |
+
"macOS has no minimum password age setting; rely on password history instead."
|
| 102 |
+
]
|
| 103 |
},
|
| 104 |
"verify": "Re-run the audit; Minimum password age should read {PASSWORD_MIN_AGE_DAYS} day(s) or more."
|
| 105 |
},
|
| 106 |
"password.complexity": {
|
| 107 |
"why": "Mixing character types makes passwords harder to guess.",
|
| 108 |
+
"risk": {
|
| 109 |
+
"level": "Medium",
|
| 110 |
+
"reason": "Simple passwords are cracked quickly by dictionary attacks."
|
| 111 |
+
},
|
| 112 |
"fix": {
|
| 113 |
+
"windows": [
|
| 114 |
+
"Set Group Policy: Account Policies > Password Policy > Password must meet complexity requirements = Enabled."
|
| 115 |
+
],
|
| 116 |
+
"linux": [
|
| 117 |
+
"Enable pam_pwquality and set minclass = 3 (or 4) in /etc/security/pwquality.conf."
|
| 118 |
+
],
|
| 119 |
+
"macos": [
|
| 120 |
+
"Deploy a Passcode configuration profile with requireAlphanumeric = true (and minComplexChars = 1 if special characters are required)."
|
| 121 |
+
]
|
| 122 |
},
|
| 123 |
"verify": "Re-run the audit; Password complexity should be Compliant."
|
| 124 |
},
|
| 125 |
"lockout.threshold": {
|
| 126 |
"why": "Locking an account after a few wrong passwords stops online guessing.",
|
| 127 |
+
"risk": {
|
| 128 |
+
"level": "Medium",
|
| 129 |
+
"reason": "Unlimited attempts allow brute-force attacks against local accounts."
|
| 130 |
+
},
|
| 131 |
"fix": {
|
| 132 |
+
"windows": [
|
| 133 |
+
"Set Group Policy: Account Policies > Account Lockout Policy > Account lockout threshold = {LOCKOUT_THRESHOLD_MAX} invalid attempts (or fewer, not 0).",
|
| 134 |
+
"For a stand-alone device: net accounts /lockoutthreshold:{LOCKOUT_THRESHOLD_MAX}"
|
| 135 |
+
],
|
| 136 |
+
"linux": [
|
| 137 |
+
"Enable pam_faillock in the PAM auth stack (RHEL: authselect enable-feature with-faillock; Debian/Ubuntu: add the preauth/authfail lines described in pam_faillock(8)).",
|
| 138 |
+
"Set deny = {LOCKOUT_THRESHOLD_MAX} in /etc/security/faillock.conf."
|
| 139 |
+
],
|
| 140 |
+
"macos": [
|
| 141 |
+
"Deploy a Passcode configuration profile with maxFailedAttempts = {LOCKOUT_THRESHOLD_MAX}."
|
| 142 |
+
]
|
| 143 |
},
|
| 144 |
"verify": "Re-run the audit; Account lockout threshold should be between 1 and {LOCKOUT_THRESHOLD_MAX}."
|
| 145 |
},
|
| 146 |
"lockout.duration": {
|
| 147 |
"why": "The lockout must last long enough to slow down repeated guessing.",
|
| 148 |
+
"risk": {
|
| 149 |
+
"level": "Low",
|
| 150 |
+
"reason": "A short lockout lets attackers resume guessing almost at once."
|
| 151 |
+
},
|
| 152 |
"fix": {
|
| 153 |
+
"windows": [
|
| 154 |
+
"Set Group Policy: Account Lockout Policy > Account lockout duration = {LOCKOUT_DURATION_MIN_MINUTES} minutes or more.",
|
| 155 |
+
"For a stand-alone device: net accounts /lockoutduration:{LOCKOUT_DURATION_MIN_MINUTES}"
|
| 156 |
+
],
|
| 157 |
+
"linux": [
|
| 158 |
+
"Set unlock_time = {LOCKOUT_DURATION_SECONDS} (seconds) in /etc/security/faillock.conf."
|
| 159 |
+
],
|
| 160 |
+
"macos": [
|
| 161 |
+
"Deploy a Passcode configuration profile with minutesUntilFailedLoginReset = {LOCKOUT_DURATION_MIN_MINUTES}."
|
| 162 |
+
]
|
| 163 |
},
|
| 164 |
"verify": "Re-run the audit; Account lockout duration should be {LOCKOUT_DURATION_MIN_MINUTES} minutes or more."
|
| 165 |
},
|
| 166 |
"lockout.observation": {
|
| 167 |
"why": "Failed attempts must be counted over a long enough window to detect slow guessing.",
|
| 168 |
+
"risk": {
|
| 169 |
+
"level": "Low",
|
| 170 |
+
"reason": "A short window lets attackers pace their guesses to avoid lockout."
|
| 171 |
+
},
|
| 172 |
"fix": {
|
| 173 |
+
"windows": [
|
| 174 |
+
"Set Group Policy: Account Lockout Policy > Reset account lockout counter after = {LOCKOUT_OBSERVATION_MIN_MINUTES} minutes or more.",
|
| 175 |
+
"For a stand-alone device: net accounts /lockoutwindow:{LOCKOUT_OBSERVATION_MIN_MINUTES}"
|
| 176 |
+
],
|
| 177 |
+
"linux": [
|
| 178 |
+
"Set fail_interval = {LOCKOUT_OBSERVATION_SECONDS} (seconds) in /etc/security/faillock.conf."
|
| 179 |
+
],
|
| 180 |
+
"macos": [
|
| 181 |
+
"On macOS the counter resets when the lockout ends; set minutesUntilFailedLoginReset = {LOCKOUT_OBSERVATION_MIN_MINUTES} in the Passcode profile."
|
| 182 |
+
]
|
| 183 |
},
|
| 184 |
"verify": "Re-run the audit; Lockout observation period should be {LOCKOUT_OBSERVATION_MIN_MINUTES} minutes or more."
|
| 185 |
},
|
| 186 |
"malware.antivirus": {
|
| 187 |
"why": "Anti-malware software detects and blocks known malicious files and behaviour.",
|
| 188 |
+
"risk": {
|
| 189 |
+
"level": "High",
|
| 190 |
+
"reason": "Without working anti-malware, ransomware and info-stealers can run unchallenged."
|
| 191 |
+
},
|
| 192 |
"fix": {
|
| 193 |
+
"windows": [
|
| 194 |
+
"Turn Microsoft Defender Antivirus back on (Windows Security > Virus & threat protection) or make sure the third-party product is installed, enabled and reporting to Security Center."
|
| 195 |
+
],
|
| 196 |
+
"linux": [
|
| 197 |
+
"Install an endpoint protection product, or ClamAV: apt install clamav clamav-daemon (RHEL: dnf install clamav clamd), then systemctl enable --now clamav-daemon."
|
| 198 |
+
],
|
| 199 |
+
"macos": [
|
| 200 |
+
"XProtect is built in; if it is missing or damaged, reinstall the latest macOS update. Add a managed endpoint protection product if your policy requires on-access scanning."
|
| 201 |
+
],
|
| 202 |
+
"android": [
|
| 203 |
+
"Play Store › profile picture › Play Protect › Settings: turn on 'Scan apps with Play Protect'."
|
| 204 |
+
]
|
| 205 |
},
|
| 206 |
"verify": "Re-run the audit; Antivirus should be Compliant."
|
| 207 |
},
|
| 208 |
"malware.service": {
|
| 209 |
"why": "The malware protection service must be running for protection to work.",
|
| 210 |
+
"risk": {
|
| 211 |
+
"level": "High",
|
| 212 |
+
"reason": "A stopped protection service leaves the device unprotected."
|
| 213 |
+
},
|
| 214 |
"fix": {
|
| 215 |
+
"windows": [
|
| 216 |
+
"Re-enable Microsoft Defender: check that no Group Policy sets 'Turn off Microsoft Defender Antivirus', then restart the device."
|
| 217 |
+
],
|
| 218 |
+
"linux": [
|
| 219 |
+
"systemctl enable --now clamav-daemon (RHEL: clamd@scan)."
|
| 220 |
+
],
|
| 221 |
+
"macos": [
|
| 222 |
+
"Re-enable Gatekeeper: sudo spctl --master-enable. Keep 'Install Security Responses and system files' on in Software Update."
|
| 223 |
+
]
|
| 224 |
},
|
| 225 |
"verify": "Re-run the audit; the malware protection service should be Compliant."
|
| 226 |
},
|
| 227 |
"malware.realtime": {
|
| 228 |
"why": "Real-time protection scans files and processes as they are used.",
|
| 229 |
+
"risk": {
|
| 230 |
+
"level": "High",
|
| 231 |
+
"reason": "Without it, malware is only found by scheduled scans, after it has run."
|
| 232 |
+
},
|
| 233 |
"fix": {
|
| 234 |
+
"windows": [
|
| 235 |
+
"Set-MpPreference -DisableRealtimeMonitoring $false, and remove any policy that disables real-time protection."
|
| 236 |
+
]
|
| 237 |
},
|
| 238 |
"verify": "Re-run the audit; Real-time protection should be on."
|
| 239 |
},
|
| 240 |
"malware.on_access": {
|
| 241 |
"why": "On-access scanning checks files when they are opened or written.",
|
| 242 |
+
"risk": {
|
| 243 |
+
"level": "Medium",
|
| 244 |
+
"reason": "Malicious files can be opened before any scheduled scan finds them."
|
| 245 |
+
},
|
| 246 |
"fix": {
|
| 247 |
+
"windows": [
|
| 248 |
+
"Enable real-time protection in Microsoft Defender (on-access protection follows it)."
|
| 249 |
+
],
|
| 250 |
+
"linux": [
|
| 251 |
+
"Configure OnAccessIncludePath in /etc/clamav/clamd.conf and enable clamonacc (systemctl enable --now clamav-clamonacc), or use an endpoint product with on-access scanning."
|
| 252 |
+
],
|
| 253 |
+
"macos": [
|
| 254 |
+
"Not provided by macOS itself; deploy a managed endpoint protection product if required."
|
| 255 |
+
]
|
| 256 |
},
|
| 257 |
"verify": "Re-run the audit; On-access malware protection should be Compliant."
|
| 258 |
},
|
| 259 |
"malware.tamper": {
|
| 260 |
"why": "Tamper protection stops malware or users switching off Defender.",
|
| 261 |
+
"risk": {
|
| 262 |
+
"level": "Medium",
|
| 263 |
+
"reason": "Attackers commonly disable antivirus before deploying ransomware."
|
| 264 |
+
},
|
| 265 |
"fix": {
|
| 266 |
+
"windows": [
|
| 267 |
+
"Turn on Tamper Protection in Windows Security > Virus & threat protection settings, or centrally through Microsoft Intune / Defender for Endpoint (it cannot be set with PowerShell)."
|
| 268 |
+
]
|
| 269 |
},
|
| 270 |
"verify": "Re-run the audit; Tamper protection should be on."
|
| 271 |
},
|
| 272 |
"malware.signatures": {
|
| 273 |
"why": "Anti-malware can only detect threats it knows about; signatures must be current.",
|
| 274 |
+
"risk": {
|
| 275 |
+
"level": "High",
|
| 276 |
+
"reason": "Outdated signatures miss recent malware."
|
| 277 |
+
},
|
| 278 |
"fix": {
|
| 279 |
+
"windows": [
|
| 280 |
+
"Run Update-MpSignature and check that the device can reach Windows Update or your update server."
|
| 281 |
+
],
|
| 282 |
+
"linux": [
|
| 283 |
+
"systemctl enable --now clamav-freshclam, or run freshclam once to update now."
|
| 284 |
+
],
|
| 285 |
+
"macos": [
|
| 286 |
+
"Keep 'Install Security Responses and system files' on (Software Update > Automatic Updates); run softwareupdate --background to fetch now."
|
| 287 |
+
]
|
| 288 |
},
|
| 289 |
"verify": "Re-run the audit; signatures should be within the allowed age."
|
| 290 |
},
|
| 291 |
"malware.signature_updates": {
|
| 292 |
"why": "Signature updates must happen automatically so protection stays current without user action.",
|
| 293 |
+
"risk": {
|
| 294 |
+
"level": "Medium",
|
| 295 |
+
"reason": "Manual updates are forgotten, leaving protection outdated."
|
| 296 |
+
},
|
| 297 |
"fix": {
|
| 298 |
+
"linux": [
|
| 299 |
+
"systemctl enable --now clamav-freshclam."
|
| 300 |
+
],
|
| 301 |
+
"macos": [
|
| 302 |
+
"Turn on 'Install Security Responses and system files' in System Settings > General > Software Update > Automatic Updates, or set ConfigDataInstall and CriticalUpdateInstall to true by configuration profile."
|
| 303 |
+
]
|
| 304 |
},
|
| 305 |
"verify": "Re-run the audit; signature updates should be automatic."
|
| 306 |
},
|
| 307 |
"firewall.enabled": {
|
| 308 |
"why": "A host firewall blocks unwanted network connections to the device, especially on public networks.",
|
| 309 |
+
"risk": {
|
| 310 |
+
"level": "High",
|
| 311 |
+
"reason": "Network services on the device are exposed to anyone on the same network."
|
| 312 |
+
},
|
| 313 |
"fix": {
|
| 314 |
+
"windows": [
|
| 315 |
+
"Set-NetFirewallProfile -Profile Domain,Private,Public -Enabled True, or enforce it with Group Policy/Intune."
|
| 316 |
+
],
|
| 317 |
+
"linux": [
|
| 318 |
+
"Debian/Ubuntu: ufw default deny incoming; ufw default allow outgoing; ufw enable. RHEL: systemctl enable --now firewalld."
|
| 319 |
+
],
|
| 320 |
+
"macos": [
|
| 321 |
+
"sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on, or enforce the firewall by configuration profile."
|
| 322 |
+
]
|
| 323 |
},
|
| 324 |
"verify": "Re-run the audit; the firewall should be on."
|
| 325 |
},
|
| 326 |
"firewall.inbound": {
|
| 327 |
"why": "Unsolicited inbound connections should be blocked unless a service is explicitly allowed.",
|
| 328 |
+
"risk": {
|
| 329 |
+
"level": "High",
|
| 330 |
+
"reason": "Open inbound access lets attackers reach vulnerable services directly."
|
| 331 |
+
},
|
| 332 |
"fix": {
|
| 333 |
+
"windows": [
|
| 334 |
+
"Set-NetFirewallProfile -Profile Domain,Private,Public -DefaultInboundAction Block."
|
| 335 |
+
],
|
| 336 |
+
"linux": [
|
| 337 |
+
"ufw default deny incoming (firewalld: keep the default zone target at default/REJECT)."
|
| 338 |
+
],
|
| 339 |
+
"macos": [
|
| 340 |
+
"Turn on stealth mode: sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setstealthmode on."
|
| 341 |
+
]
|
| 342 |
},
|
| 343 |
"verify": "Re-run the audit; the inbound policy should be block/deny."
|
| 344 |
},
|
| 345 |
"firewall.outbound": {
|
| 346 |
"why": "The baseline fixes the default outbound policy so that it is consistent across devices.",
|
| 347 |
+
"risk": {
|
| 348 |
+
"level": "Low",
|
| 349 |
+
"reason": "A different outbound policy either breaks applications or deviates from the agreed design."
|
| 350 |
+
},
|
| 351 |
"fix": {
|
| 352 |
+
"windows": [
|
| 353 |
+
"Set-NetFirewallProfile -Profile Domain,Private,Public -DefaultOutboundAction {FIREWALL_OUTBOUND_TITLE}."
|
| 354 |
+
],
|
| 355 |
+
"linux": [
|
| 356 |
+
"ufw default {FIREWALL_OUTBOUND} outgoing (or the equivalent nftables output policy)."
|
| 357 |
+
],
|
| 358 |
+
"macos": [
|
| 359 |
+
"The macOS application firewall does not filter outbound traffic; use a network filter product if outbound blocking is required."
|
| 360 |
+
]
|
| 361 |
},
|
| 362 |
"verify": "Re-run the audit; the outbound policy should read {FIREWALL_OUTBOUND}."
|
| 363 |
},
|
| 364 |
"screen_lock.enabled": {
|
| 365 |
"why": "An idle device should lock itself so nobody can use it while the owner is away.",
|
| 366 |
+
"risk": {
|
| 367 |
+
"level": "Medium",
|
| 368 |
+
"reason": "An unlocked, unattended device exposes all the user's data and accounts."
|
| 369 |
+
},
|
| 370 |
"fix": {
|
| 371 |
+
"windows": [
|
| 372 |
+
"Set Group Policy: Local Policies > Security Options > Interactive logon: Machine inactivity limit = {SCREEN_LOCK_MAX_SECONDS} seconds, or enforce a password-protected screen saver by Group Policy/Intune."
|
| 373 |
+
],
|
| 374 |
+
"linux": [
|
| 375 |
+
"For GNOME: gsettings set org.gnome.desktop.screensaver lock-enabled true, and enforce it for all users with a locked dconf setting in /etc/dconf/db/local.d/."
|
| 376 |
+
],
|
| 377 |
+
"macos": [
|
| 378 |
+
"System Settings > Lock Screen: set the screen saver and display-off times and 'Require password' = Immediately, or enforce with a configuration profile."
|
| 379 |
+
],
|
| 380 |
+
"ios": [
|
| 381 |
+
"Settings › Face ID & Passcode › Turn Passcode On; for work phones the employer can require it with a configuration profile."
|
| 382 |
+
],
|
| 383 |
+
"android": [
|
| 384 |
+
"Settings › Security & privacy › Device unlock › Screen lock: choose PIN or Password."
|
| 385 |
+
]
|
| 386 |
},
|
| 387 |
"verify": "Re-run the audit; Screen lock should be on."
|
| 388 |
},
|
| 389 |
"screen_lock.timeout": {
|
| 390 |
"why": "The shorter the idle time before locking, the smaller the window for misuse.",
|
| 391 |
+
"risk": {
|
| 392 |
+
"level": "Medium",
|
| 393 |
+
"reason": "A long or missing timeout leaves unattended devices open."
|
| 394 |
+
},
|
| 395 |
"fix": {
|
| 396 |
+
"windows": [
|
| 397 |
+
"Set Group Policy: Computer Configuration > Windows Settings > Security Settings > Local Policies > Security Options > Interactive logon: Machine inactivity limit = {SCREEN_LOCK_MAX_SECONDS} seconds (or less)."
|
| 398 |
+
],
|
| 399 |
+
"linux": [
|
| 400 |
+
"gsettings set org.gnome.desktop.session idle-delay {SCREEN_LOCK_MAX_SECONDS} and gsettings set org.gnome.desktop.screensaver lock-delay 0; enforce with locked dconf settings."
|
| 401 |
+
],
|
| 402 |
+
"macos": [
|
| 403 |
+
"Set the screen saver to start within {SCREEN_LOCK_MAX_MINUTES} minutes (idleTime in com.apple.screensaver) and require the password immediately, ideally by configuration profile."
|
| 404 |
+
],
|
| 405 |
+
"ios": [
|
| 406 |
+
"Settings › Display & Brightness › Auto-Lock: choose {SCREEN_LOCK_MAX_MINUTES} minutes or less (not Never)."
|
| 407 |
+
],
|
| 408 |
+
"android": [
|
| 409 |
+
"Settings › Display › Screen timeout: choose {SCREEN_LOCK_MAX_MINUTES} minutes or less, and set 'Lock after screen timeout' to Immediately where the phone offers it."
|
| 410 |
+
]
|
| 411 |
},
|
| 412 |
"verify": "Re-run the audit; Screen lock timeout should be {SCREEN_LOCK_MAX_SECONDS} seconds or less."
|
| 413 |
},
|
| 414 |
"screen_lock.password": {
|
| 415 |
"why": "Unlocking the screen must require the user's password.",
|
| 416 |
+
"risk": {
|
| 417 |
+
"level": "Medium",
|
| 418 |
+
"reason": "A screen lock that needs no password does not protect anything."
|
| 419 |
+
},
|
| 420 |
"fix": {
|
| 421 |
+
"windows": [
|
| 422 |
+
"Set the machine inactivity limit by Group Policy (this always requires sign-in), or enable 'Password protect the screen saver' for users by Group Policy."
|
| 423 |
+
],
|
| 424 |
+
"linux": [
|
| 425 |
+
"Keep org.gnome.desktop.screensaver lock-enabled true and org.gnome.desktop.lockdown disable-lock-screen false."
|
| 426 |
+
],
|
| 427 |
+
"macos": [
|
| 428 |
+
"System Settings > Lock Screen > 'Require password after screen saver begins or display is turned off' = Immediately (askForPasswordDelay = 0 by profile)."
|
| 429 |
+
]
|
| 430 |
},
|
| 431 |
"verify": "Re-run the audit; the password requirement should be Compliant."
|
| 432 |
},
|
| 433 |
"usb.read": {
|
| 434 |
"why": "The baseline sets whether USB storage may be read; reading lets malware and unapproved data enter.",
|
| 435 |
+
"risk": {
|
| 436 |
+
"level": "Low",
|
| 437 |
+
"reason": "The setting differs from the organisation's agreed USB policy."
|
| 438 |
+
},
|
| 439 |
"fix": {
|
| 440 |
+
"windows": [
|
| 441 |
+
"Set Group Policy: Computer Configuration > Administrative Templates > System > Removable Storage Access > Removable Disks: Deny read access = {USB_READ_POLICY}."
|
| 442 |
+
],
|
| 443 |
+
"linux": [
|
| 444 |
+
"To block USB storage completely: echo 'install usb-storage /bin/false' > /etc/modprobe.d/usb-storage.conf, or use USBGuard."
|
| 445 |
+
],
|
| 446 |
+
"macos": [
|
| 447 |
+
"Restrict external storage with your MDM or an endpoint DLP product."
|
| 448 |
+
]
|
| 449 |
},
|
| 450 |
"verify": "Re-run the audit; USB storage read should read {USB_READ}."
|
| 451 |
},
|
| 452 |
"usb.write": {
|
| 453 |
"why": "Writing to USB drives is a common way for data to leave the organisation, often unencrypted.",
|
| 454 |
+
"risk": {
|
| 455 |
+
"level": "Medium",
|
| 456 |
+
"reason": "Personal or confidential data can be copied to an unmanaged, unencrypted drive and lost."
|
| 457 |
+
},
|
| 458 |
"fix": {
|
| 459 |
+
"windows": [
|
| 460 |
+
"Set Group Policy: Administrative Templates > System > Removable Storage Access > Removable Disks: Deny write access = Enabled, or require BitLocker To Go ('Deny write access to removable drives not protected by BitLocker')."
|
| 461 |
+
],
|
| 462 |
+
"linux": [
|
| 463 |
+
"Make USB storage read-only with a udev rule, or block it with USBGuard or the usb-storage modprobe rule."
|
| 464 |
+
],
|
| 465 |
+
"macos": [
|
| 466 |
+
"Restrict external storage to read-only or deny it with your MDM or an endpoint DLP product."
|
| 467 |
+
]
|
| 468 |
},
|
| 469 |
"verify": "Re-run the audit; USB storage write should read {USB_WRITE}."
|
| 470 |
},
|
| 471 |
"admin.approved": {
|
| 472 |
"why": "A known, approved IT account should manage the device so that support does not depend on users' own admin rights.",
|
| 473 |
+
"risk": {
|
| 474 |
+
"level": "Medium",
|
| 475 |
+
"reason": "Without an approved admin account, IT tends to rely on employees' accounts having admin rights."
|
| 476 |
+
},
|
| 477 |
"fix": {
|
| 478 |
+
"windows": [
|
| 479 |
+
"Add the approved IT administrator account (listed in APPROVED_ADMINS) to the local Administrators group, ideally managed with Windows LAPS or Intune."
|
| 480 |
+
],
|
| 481 |
+
"linux": [
|
| 482 |
+
"Add the approved IT account to the sudo (Debian/Ubuntu) or wheel (RHEL) group: usermod -aG sudo <account>."
|
| 483 |
+
],
|
| 484 |
+
"macos": [
|
| 485 |
+
"Create or promote the approved IT account: sudo dseditgroup -o edit -a <account> -t user admin, ideally managed by your MDM."
|
| 486 |
+
]
|
| 487 |
},
|
| 488 |
"verify": "Re-run the audit; Approved IT administrator should list the approved account."
|
| 489 |
},
|
| 490 |
"admin.unauthorized": {
|
| 491 |
"why": "Everyday user accounts with administrator rights let malware and mistakes change anything on the device.",
|
| 492 |
+
"risk": {
|
| 493 |
+
"level": "High",
|
| 494 |
+
"reason": "Malware running as an administrator can disable protection, install persistence and steal credentials."
|
| 495 |
+
},
|
| 496 |
"fix": {
|
| 497 |
+
"windows": [
|
| 498 |
+
"Remove the listed accounts from the local Administrators group: Remove-LocalGroupMember -SID S-1-5-32-544 -Member <account>; give elevation through an approved process (e.g. Windows LAPS, an endpoint privilege management tool)."
|
| 499 |
+
],
|
| 500 |
+
"linux": [
|
| 501 |
+
"Remove the listed accounts from the sudo/wheel groups (gpasswd -d <account> sudo) and remove their entries from /etc/sudoers.d."
|
| 502 |
+
],
|
| 503 |
+
"macos": [
|
| 504 |
+
"Demote the listed accounts: sudo dseditgroup -o edit -d <account> -t user admin."
|
| 505 |
+
]
|
| 506 |
},
|
| 507 |
"verify": "Re-run the audit; Unauthorized administrator accounts should read none."
|
| 508 |
},
|
| 509 |
"encryption.enabled": {
|
| 510 |
"why": "Full-disk encryption keeps data unreadable if the device is lost or stolen.",
|
| 511 |
+
"risk": {
|
| 512 |
+
"level": "High",
|
| 513 |
+
"reason": "A lost unencrypted laptop is a personal data breach that may have to be reported."
|
| 514 |
+
},
|
| 515 |
"fix": {
|
| 516 |
+
"windows": [
|
| 517 |
+
"Turn on BitLocker for the system drive (Enable-BitLocker -MountPoint C: -EncryptionMethod XtsAes256 -TpmProtector), preferably through Intune or Group Policy with key backup."
|
| 518 |
+
],
|
| 519 |
+
"linux": [
|
| 520 |
+
"Disk encryption (LUKS) is usually chosen at installation; plan a reinstall with encryption, or an offline re-encryption with cryptsetup reencrypt after a full backup."
|
| 521 |
+
],
|
| 522 |
+
"macos": [
|
| 523 |
+
"Turn on FileVault (System Settings > Privacy & Security > FileVault, or sudo fdesetup enable), ideally enforced and escrowed by your MDM."
|
| 524 |
+
],
|
| 525 |
+
"ios": [
|
| 526 |
+
"Set a passcode (Settings › Face ID & Passcode): iOS data protection encrypts the phone once a passcode is set."
|
| 527 |
+
],
|
| 528 |
+
"android": [
|
| 529 |
+
"Update to a supported Android version (10 or later encrypts by default) or replace the phone; set a PIN or password screen lock."
|
| 530 |
+
]
|
| 531 |
},
|
| 532 |
"verify": "Re-run the audit; Encryption should be Compliant."
|
| 533 |
},
|
| 534 |
"encryption.protection": {
|
| 535 |
"why": "Encryption only protects the data when it is active, fully applied and uses a strong cipher.",
|
| 536 |
+
"risk": {
|
| 537 |
+
"level": "High",
|
| 538 |
+
"reason": "Suspended or partial encryption leaves data readable on a lost device."
|
| 539 |
+
},
|
| 540 |
"fix": {
|
| 541 |
+
"windows": [
|
| 542 |
+
"Resume protection if it is suspended (Resume-BitLocker -MountPoint C:) and let encryption finish; use an AES-XTS method."
|
| 543 |
+
],
|
| 544 |
+
"linux": [
|
| 545 |
+
"Use LUKS2 with aes-xts-plain64 and a 512-bit key (AES-256) for new installations; plan migration of weaker setups."
|
| 546 |
+
],
|
| 547 |
+
"macos": [
|
| 548 |
+
"Let FileVault encryption finish; keep the Mac on power until fdesetup status reports FileVault is On without progress."
|
| 549 |
+
]
|
| 550 |
},
|
| 551 |
"verify": "Re-run the audit; Disk encryption protection should be Compliant."
|
| 552 |
},
|
| 553 |
"encryption.recovery": {
|
| 554 |
"why": "A recovery key lets IT unlock the disk if the user forgets the password or the hardware changes.",
|
| 555 |
+
"risk": {
|
| 556 |
+
"level": "Medium",
|
| 557 |
+
"reason": "Without a recovery key, an encrypted disk can become permanently unreadable."
|
| 558 |
+
},
|
| 559 |
"fix": {
|
| 560 |
+
"windows": [
|
| 561 |
+
"Add a recovery password: Add-BitLockerKeyProtector -MountPoint C: -RecoveryPasswordProtector, and back it up to Entra ID (BackupToAAD-BitLockerKeyProtector) or Active Directory."
|
| 562 |
+
],
|
| 563 |
+
"linux": [
|
| 564 |
+
"Add a second key slot holding a recovery passphrase: cryptsetup luksAddKey <device>, and store it in your password vault."
|
| 565 |
+
],
|
| 566 |
+
"macos": [
|
| 567 |
+
"Create or rotate a personal recovery key (sudo fdesetup changerecovery -personal) and escrow it to your MDM."
|
| 568 |
+
]
|
| 569 |
},
|
| 570 |
"verify": "Re-run the audit; Disk encryption recovery should be Compliant."
|
| 571 |
},
|
| 572 |
"usb.device_access": {
|
| 573 |
"why": "Blocking USB mass storage devices completely is the strongest protection against data leaving on, or malware arriving from, removable drives.",
|
| 574 |
+
"risk": {
|
| 575 |
+
"level": "Low",
|
| 576 |
+
"reason": "The device access setting differs from the organisation's agreed USB policy."
|
| 577 |
+
},
|
| 578 |
"fix": {
|
| 579 |
+
"windows": [
|
| 580 |
+
"To block USB storage completely: Group Policy > Administrative Templates > System > Removable Storage Access > All Removable Storage classes: Deny all access = Enabled, or set the USBSTOR service Start value to 4."
|
| 581 |
+
],
|
| 582 |
+
"linux": [
|
| 583 |
+
"To block USB storage completely: echo 'install usb-storage /bin/false' > /etc/modprobe.d/usb-storage.conf, or enable USBGuard with ImplicitPolicyTarget=block."
|
| 584 |
+
],
|
| 585 |
+
"macos": [
|
| 586 |
+
"Block or allow external storage with your MDM (Restrictions / device control) or an endpoint DLP product."
|
| 587 |
+
]
|
| 588 |
},
|
| 589 |
"verify": "Re-run the audit; USB storage device access should read {USB_DEVICE_ACCESS}."
|
| 590 |
},
|
| 591 |
"user.account_type": {
|
| 592 |
"why": "Day-to-day work under an administrator account lets any malware or mistake change the whole system.",
|
| 593 |
+
"risk": {
|
| 594 |
+
"level": "High",
|
| 595 |
+
"reason": "Malware started by the signed-in user runs with full administrator rights."
|
| 596 |
+
},
|
| 597 |
"fix": {
|
| 598 |
+
"windows": [
|
| 599 |
+
"Make the signed-in user a Standard user (Settings > Accounts > Other users > Change account type, or Remove-LocalGroupMember -SID S-1-5-32-544 -Member <account>) and use a separate approved admin account or LAPS for elevation."
|
| 600 |
+
],
|
| 601 |
+
"linux": [
|
| 602 |
+
"Remove the signed-in user from sudo/wheel (gpasswd -d <account> sudo) and use a separate approved admin account."
|
| 603 |
+
],
|
| 604 |
+
"macos": [
|
| 605 |
+
"Make the signed-in user a Standard user (System Settings > Users & Groups, or sudo dseditgroup -o edit -d <account> -t user admin) and keep a separate approved admin account."
|
| 606 |
+
]
|
| 607 |
},
|
| 608 |
"verify": "Re-run the audit; Signed-in user account type should read Standard."
|
| 609 |
},
|
| 610 |
"vuln.pending_updates": {
|
| 611 |
"why": "Security updates that are offered but not installed leave known, published vulnerabilities open.",
|
| 612 |
+
"risk": {
|
| 613 |
+
"level": "High",
|
| 614 |
+
"reason": "Attackers target published vulnerabilities quickly after the fix is released."
|
| 615 |
+
},
|
| 616 |
"fix": {
|
| 617 |
+
"windows": [
|
| 618 |
+
"Install the pending updates now (Settings > Windows Update > Install all), restart if asked, and check that automatic updates are not paused."
|
| 619 |
+
],
|
| 620 |
+
"linux": [
|
| 621 |
+
"Install pending security updates: apt update && apt upgrade (RHEL: dnf upgrade --security), then reboot if a new kernel was installed."
|
| 622 |
+
],
|
| 623 |
+
"macos": [
|
| 624 |
+
"Install the pending updates: System Settings > General > Software Update, or softwareupdate -ia, then restart if asked."
|
| 625 |
+
],
|
| 626 |
+
"ios": [
|
| 627 |
+
"Settings › General › Software Update: install the latest iOS version offered."
|
| 628 |
+
],
|
| 629 |
+
"android": [
|
| 630 |
+
"Settings › System › Software updates (and Security & privacy › Updates): install the latest system and Google Play system updates. If the security patch stays older than {MOBILE_PATCH_MAX_AGE_DAYS} days because the maker no longer sends updates, replace the phone."
|
| 631 |
+
]
|
| 632 |
},
|
| 633 |
"verify": "Re-run the audit; pending security updates should be {MAX_PENDING_SECURITY_UPDATES} or fewer."
|
| 634 |
},
|
| 635 |
"vuln.outdated_apps": {
|
| 636 |
"why": "Browsers, office suites, PDF readers and meeting apps are common attack targets and need updates like the OS.",
|
| 637 |
+
"risk": {
|
| 638 |
+
"level": "Medium",
|
| 639 |
+
"reason": "Outdated applications contain known vulnerabilities that attackers exploit through documents and web pages."
|
| 640 |
+
},
|
| 641 |
"fix": {
|
| 642 |
+
"windows": [
|
| 643 |
+
"Update the listed applications (winget upgrade --all, or each app's own updater) and manage updates centrally with Intune or a patch management tool."
|
| 644 |
+
],
|
| 645 |
+
"linux": [
|
| 646 |
+
"Update packages and app stores: apt upgrade, snap refresh, flatpak update."
|
| 647 |
+
],
|
| 648 |
+
"macos": [
|
| 649 |
+
"Update the listed applications from the App Store or their own updaters (or brew upgrade for Homebrew apps), and manage updates centrally with your MDM."
|
| 650 |
+
],
|
| 651 |
+
"ios": [
|
| 652 |
+
"Settings › App Store: turn on App Updates; then App Store › account picture › update all pending apps."
|
| 653 |
+
],
|
| 654 |
+
"android": [
|
| 655 |
+
"Play Store › profile picture › Settings › Network preferences › Auto-update apps: Over any network or Over Wi-Fi only; then Manage apps & device › Update all."
|
| 656 |
+
]
|
| 657 |
},
|
| 658 |
"verify": "Re-run the audit; outdated applications should be {MAX_OUTDATED_APPS} or fewer."
|
| 659 |
},
|
| 660 |
"vuln.known_vulnerabilities": {
|
| 661 |
"why": "Installed software versions with published critical or high vulnerabilities give attackers a ready-made way in.",
|
| 662 |
+
"risk": {
|
| 663 |
+
"level": "High",
|
| 664 |
+
"reason": "Critical and high vulnerabilities often allow remote code execution or privilege escalation."
|
| 665 |
+
},
|
| 666 |
"fix": {
|
| 667 |
+
"all": [
|
| 668 |
+
"Update or remove each listed software package to a version without the listed vulnerabilities; where no fix exists yet, apply the vendor's workaround or restrict the software's use."
|
| 669 |
+
],
|
| 670 |
+
"windows": [
|
| 671 |
+
"Use winget upgrade or the vendor's installer to reach a fixed version."
|
| 672 |
+
],
|
| 673 |
+
"linux": [
|
| 674 |
+
"Use the package manager to reach a fixed version (apt / dnf / snap / flatpak)."
|
| 675 |
+
],
|
| 676 |
+
"macos": [
|
| 677 |
+
"Use the App Store, the vendor's updater or brew upgrade to reach a fixed version."
|
| 678 |
+
],
|
| 679 |
+
"ios": [
|
| 680 |
+
"Settings › General › Software Update: install the newest iOS version; if the iPhone cannot run a version without the listed vulnerabilities, replace it."
|
| 681 |
+
]
|
| 682 |
},
|
| 683 |
"verify": "Re-run the audit; critical or high vulnerabilities should be {MAX_CRITICAL_HIGH_VULNERABILITIES} or fewer."
|
| 684 |
},
|
|
|
|
| 685 |
"gdpr.auto_updates": {
|
| 686 |
"why": "Security fixes must be installed automatically so that known vulnerabilities are closed quickly.",
|
| 687 |
+
"risk": {
|
| 688 |
+
"level": "High",
|
| 689 |
+
"reason": "Unpatched devices are the most common way attackers get in."
|
| 690 |
+
},
|
| 691 |
"fix": {
|
| 692 |
+
"windows": [
|
| 693 |
+
"Set Group Policy: Administrative Templates > Windows Components > Windows Update > Configure Automatic Updates = 4 (auto download and schedule the install), or use Windows Update for Business / Intune; make sure the Windows Update service is not disabled."
|
| 694 |
+
],
|
| 695 |
+
"linux": [
|
| 696 |
+
"Debian/Ubuntu: apt install unattended-upgrades && dpkg-reconfigure -plow unattended-upgrades. RHEL: dnf install dnf-automatic && systemctl enable --now dnf-automatic-install.timer."
|
| 697 |
+
],
|
| 698 |
+
"macos": [
|
| 699 |
+
"Turn on all options in System Settings > General > Software Update > Automatic Updates, or enforce them with a configuration profile (AutomaticallyInstallMacOSUpdates = true)."
|
| 700 |
+
],
|
| 701 |
+
"ios": [
|
| 702 |
+
"Settings › General › Software Update › Automatic Updates: turn on iOS Updates Download and Install, and Security Responses & System Files."
|
| 703 |
+
],
|
| 704 |
+
"android": [
|
| 705 |
+
"Settings › System › Software updates: turn on automatic download (names vary by phone maker)."
|
| 706 |
+
]
|
| 707 |
},
|
| 708 |
"verify": "Re-run the audit; Automatic security updates should be Compliant."
|
| 709 |
},
|
| 710 |
"gdpr.updates_recent": {
|
| 711 |
"why": "Recent installs show that patching is actually happening, not only configured.",
|
| 712 |
+
"risk": {
|
| 713 |
+
"level": "High",
|
| 714 |
+
"reason": "A device not updated for a long time is likely missing critical security fixes."
|
| 715 |
+
},
|
| 716 |
"fix": {
|
| 717 |
+
"windows": [
|
| 718 |
+
"Run Windows Update now (Settings > Windows Update > Check for updates) and investigate why updates stopped (paused updates, broken update service, metered connection)."
|
| 719 |
+
],
|
| 720 |
+
"linux": [
|
| 721 |
+
"Run apt update && apt upgrade (RHEL: dnf upgrade) and check the automatic update logs for errors."
|
| 722 |
+
],
|
| 723 |
+
"macos": [
|
| 724 |
+
"Install pending updates (System Settings > General > Software Update, or softwareupdate -ia)."
|
| 725 |
+
]
|
| 726 |
},
|
| 727 |
"verify": "Re-run the audit; the last update should be within {UPDATES_MAX_AGE_DAYS} days."
|
| 728 |
},
|
| 729 |
"gdpr.os_supported": {
|
| 730 |
"why": "An operating system that no longer receives security updates cannot be kept secure.",
|
| 731 |
+
"risk": {
|
| 732 |
+
"level": "High",
|
| 733 |
+
"reason": "New vulnerabilities in an unsupported OS are never fixed."
|
| 734 |
+
},
|
| 735 |
"fix": {
|
| 736 |
+
"windows": [
|
| 737 |
+
"Upgrade to a supported Windows 11 release (build {WINDOWS_MIN_BUILD} or later), or replace hardware that cannot run it."
|
| 738 |
+
],
|
| 739 |
+
"linux": [
|
| 740 |
+
"Upgrade to a supported release (e.g. Ubuntu {UBUNTU_MIN_VERSION} or later) or enrol in extended security maintenance."
|
| 741 |
+
],
|
| 742 |
+
"macos": [
|
| 743 |
+
"Upgrade to macOS {MACOS_MIN_MAJOR} or later, or replace Macs that cannot run a supported version."
|
| 744 |
+
],
|
| 745 |
+
"ios": [
|
| 746 |
+
"Update to iOS {IOS_MIN_MAJOR} or later in Settings › General › Software Update; if the iPhone cannot run it, replace the iPhone."
|
| 747 |
+
],
|
| 748 |
+
"android": [
|
| 749 |
+
"Update to Android {ANDROID_MIN_MAJOR} or later in Settings › System › Software updates; if the maker no longer offers it, replace the phone."
|
| 750 |
+
]
|
| 751 |
},
|
| 752 |
"verify": "Re-run the audit; Supported operating system version should be Compliant."
|
| 753 |
},
|
| 754 |
"gdpr.security_logging": {
|
| 755 |
"why": "Logon and account-change events are needed to detect and investigate a breach within the 72-hour GDPR deadline.",
|
| 756 |
+
"risk": {
|
| 757 |
+
"level": "Medium",
|
| 758 |
+
"reason": "Without these logs, a breach cannot be scoped and the notification decision becomes guesswork."
|
| 759 |
+
},
|
| 760 |
"fix": {
|
| 761 |
+
"windows": [
|
| 762 |
+
"Enable auditing: auditpol /set /subcategory:{0CCE9215-69AE-11D9-BED3-505054503030} /success:enable /failure:enable (Logon) and auditpol /set /subcategory:{0CCE9235-69AE-11D9-BED3-505054503030} /success:enable /failure:enable (User Account Management), or set the Advanced Audit Policy by Group Policy."
|
| 763 |
+
],
|
| 764 |
+
"linux": [
|
| 765 |
+
"Install and start auditd (apt install auditd; systemctl enable --now auditd) and keep the journal persistent (Storage=persistent in /etc/systemd/journald.conf)."
|
| 766 |
+
],
|
| 767 |
+
"macos": [
|
| 768 |
+
"Unified logging is built in; forward security logs to your log platform if long retention is required."
|
| 769 |
+
]
|
| 770 |
},
|
| 771 |
"verify": "Re-run the audit; Security event logging should be Compliant."
|
| 772 |
},
|
| 773 |
"gdpr.time_sync": {
|
| 774 |
"why": "Accurate clocks are needed to put events in the right order during an investigation.",
|
| 775 |
+
"risk": {
|
| 776 |
+
"level": "Low",
|
| 777 |
+
"reason": "Wrong timestamps make incident timelines unreliable."
|
| 778 |
+
},
|
| 779 |
"fix": {
|
| 780 |
+
"windows": [
|
| 781 |
+
"w32tm /config /syncfromflags:manual /manualpeerlist:\"pool.ntp.org\" /update (domain members sync from the domain automatically); make sure the Windows Time service is not disabled."
|
| 782 |
+
],
|
| 783 |
+
"linux": [
|
| 784 |
+
"timedatectl set-ntp true (or enable chronyd)."
|
| 785 |
+
],
|
| 786 |
+
"macos": [
|
| 787 |
+
"sudo systemsetup -setusingnetworktime on."
|
| 788 |
+
],
|
| 789 |
+
"ios": [
|
| 790 |
+
"Settings › General › Date & Time: turn on Set Automatically."
|
| 791 |
+
],
|
| 792 |
+
"android": [
|
| 793 |
+
"Settings › System › Date & time: turn on Set time automatically."
|
| 794 |
+
]
|
| 795 |
},
|
| 796 |
"verify": "Re-run the audit; Time synchronisation should be Compliant."
|
| 797 |
},
|
| 798 |
"gdpr.backup_configured": {
|
| 799 |
"why": "Backups make it possible to restore personal data after loss, theft, failure or ransomware.",
|
| 800 |
+
"risk": {
|
| 801 |
+
"level": "High",
|
| 802 |
+
"reason": "Data that exists only on this device is lost if the device fails or is encrypted by ransomware."
|
| 803 |
+
},
|
| 804 |
"fix": {
|
| 805 |
+
"windows": [
|
| 806 |
+
"Back up user folders with OneDrive Known Folder Move (Group Policy/Intune), File History, or the organisation's backup agent."
|
| 807 |
+
],
|
| 808 |
+
"linux": [
|
| 809 |
+
"Set up scheduled backups of /home with Deja Dup, restic or borg (or Timeshift for system snapshots plus a data backup)."
|
| 810 |
+
],
|
| 811 |
+
"macos": [
|
| 812 |
+
"Configure Time Machine (tmutil setdestination) or the organisation's backup agent."
|
| 813 |
+
],
|
| 814 |
+
"ios": [
|
| 815 |
+
"Settings › [your name] › iCloud › iCloud Backup: turn on, or use the employer's approved backup; keep work data only in company-approved apps."
|
| 816 |
+
],
|
| 817 |
+
"android": [
|
| 818 |
+
"Settings › Google › Backup: turn on Backup by Google One (or the employer's approved backup); keep work data only in company-approved apps."
|
| 819 |
+
]
|
| 820 |
},
|
| 821 |
"verify": "Re-run the audit; Backup configured should be Compliant."
|
| 822 |
},
|
| 823 |
"gdpr.backup_recent": {
|
| 824 |
"why": "A configured backup only helps if it has actually run recently.",
|
| 825 |
+
"risk": {
|
| 826 |
+
"level": "Medium",
|
| 827 |
+
"reason": "Recent work would be lost if the device failed today."
|
| 828 |
+
},
|
| 829 |
"fix": {
|
| 830 |
+
"windows": [
|
| 831 |
+
"Check the backup tool's history and run a backup now; fix whatever stopped it (full destination, signed-out OneDrive, stopped agent)."
|
| 832 |
+
],
|
| 833 |
+
"linux": [
|
| 834 |
+
"Run the backup job now and check its timer and logs."
|
| 835 |
+
],
|
| 836 |
+
"macos": [
|
| 837 |
+
"Run tmutil startbackup and check that the destination is reachable."
|
| 838 |
+
]
|
| 839 |
},
|
| 840 |
"verify": "Re-run the audit; the last backup should be within {BACKUP_MAX_AGE_DAYS} days."
|
| 841 |
},
|
| 842 |
"gdpr.removable_encryption": {
|
| 843 |
"why": "Personal data copied to removable media must be encrypted, or copying must be blocked.",
|
| 844 |
+
"risk": {
|
| 845 |
+
"level": "Medium",
|
| 846 |
+
"reason": "A lost unencrypted USB drive with personal data is a reportable breach."
|
| 847 |
+
},
|
| 848 |
"fix": {
|
| 849 |
+
"windows": [
|
| 850 |
+
"Enable 'Deny write access to removable drives not protected by BitLocker' (Group Policy: Windows Components > BitLocker Drive Encryption > Removable Data Drives), or deny USB write access."
|
| 851 |
+
],
|
| 852 |
+
"linux": [
|
| 853 |
+
"Block USB storage writes (udev read-only rule or USBGuard) and provide approved encrypted drives."
|
| 854 |
+
],
|
| 855 |
+
"macos": [
|
| 856 |
+
"Restrict external storage writes with your MDM or an endpoint DLP product and provide approved encrypted drives."
|
| 857 |
+
]
|
| 858 |
},
|
| 859 |
"verify": "Re-run the audit; Removable media encryption should be Compliant."
|
| 860 |
},
|
| 861 |
"gdpr.guest_account": {
|
| 862 |
"why": "Guest logins let people use the device without an identifiable account.",
|
| 863 |
+
"risk": {
|
| 864 |
+
"level": "Medium",
|
| 865 |
+
"reason": "Actions by a guest cannot be traced to a person, and guests may reach shared data."
|
| 866 |
+
},
|
| 867 |
"fix": {
|
| 868 |
+
"windows": [
|
| 869 |
+
"Disable the built-in Guest account: Get-LocalUser | Where-Object { $_.SID -like '*-501' } | Disable-LocalUser"
|
| 870 |
+
],
|
| 871 |
+
"linux": [
|
| 872 |
+
"Set allow-guest=false in the LightDM configuration and remove any 'guest' account with a login shell."
|
| 873 |
+
],
|
| 874 |
+
"macos": [
|
| 875 |
+
"sudo defaults write /Library/Preferences/com.apple.loginwindow GuestEnabled -bool false, and turn off guest access to shared folders."
|
| 876 |
+
]
|
| 877 |
},
|
| 878 |
"verify": "Re-run the audit; Guest account disabled should be Compliant."
|
| 879 |
},
|
| 880 |
"gdpr.remote_access": {
|
| 881 |
"why": "Remote access services let others reach the device over the network; only approved ones should be on.",
|
| 882 |
+
"risk": {
|
| 883 |
+
"level": "High",
|
| 884 |
+
"reason": "Unapproved remote access tools are a common entry point for attackers and support scams."
|
| 885 |
+
},
|
| 886 |
"fix": {
|
| 887 |
+
"windows": [
|
| 888 |
+
"Turn off the listed services if not approved: disable Remote Desktop (fDenyTSConnections = 1), stop and disable sshd/WinRM, remove unneeded shares, and uninstall unapproved tools such as TeamViewer, AnyDesk or Chrome Remote Desktop."
|
| 889 |
+
],
|
| 890 |
+
"linux": [
|
| 891 |
+
"Disable unapproved services, e.g. systemctl disable --now ssh xrdp smbd, and remove unapproved remote access tools."
|
| 892 |
+
],
|
| 893 |
+
"macos": [
|
| 894 |
+
"Turn off Remote Login, Screen Sharing, File Sharing and Remote Management in System Settings > General > Sharing, and uninstall unapproved tools (e.g. the Chrome Remote Desktop host)."
|
| 895 |
+
]
|
| 896 |
},
|
| 897 |
"verify": "Re-run the audit; only approved services ({APPROVED_REMOTE_SERVICES_TEXT}) should remain.",
|
| 898 |
"note": "If a listed service is needed, add it to APPROVED_REMOTE_SERVICES in baseline.conf and secure it (multi-factor authentication, restricted to the VPN)."
|
| 899 |
},
|
| 900 |
"gdpr.telemetry": {
|
| 901 |
"why": "Operating systems can send optional usage and diagnostic data, which may contain personal data, to the vendor.",
|
| 902 |
+
"risk": {
|
| 903 |
+
"level": "Low",
|
| 904 |
+
"reason": "More personal data than necessary leaves the organisation."
|
| 905 |
+
},
|
| 906 |
"fix": {
|
| 907 |
+
"windows": [
|
| 908 |
+
"Set Group Policy: Administrative Templates > Windows Components > Data Collection and Preview Builds > Allow Diagnostic Data = Send required diagnostic data (AllowTelemetry = 1; 0 on Enterprise/Education)."
|
| 909 |
+
],
|
| 910 |
+
"linux": [
|
| 911 |
+
"Opt out of popularity-contest (dpkg-reconfigure popularity-contest) and disable whoopsie if present (systemctl disable --now whoopsie)."
|
| 912 |
+
],
|
| 913 |
+
"macos": [
|
| 914 |
+
"Turn off System Settings > Privacy & Security > Analytics & Improvements > Share Mac Analytics and Share with App Developers, or enforce by profile."
|
| 915 |
+
],
|
| 916 |
+
"ios": [
|
| 917 |
+
"Settings › Privacy & Security › Analytics & Improvements: turn off Share iPhone Analytics."
|
| 918 |
+
],
|
| 919 |
+
"android": [
|
| 920 |
+
"Settings › Google › All services › Usage & diagnostics: turn off; also turn off the phone maker's diagnostics sharing."
|
| 921 |
+
]
|
| 922 |
},
|
| 923 |
"verify": "Re-run the audit; Diagnostic data minimised should be Compliant."
|
| 924 |
},
|
| 925 |
"gdpr.personal_data_discovery": {
|
| 926 |
"why": "Knowing where personal data is stored on endpoints is needed for the record of processing and for storage limitation.",
|
| 927 |
+
"risk": {
|
| 928 |
+
"level": "Low",
|
| 929 |
+
"reason": "Not assessed in stage 1."
|
| 930 |
+
},
|
| 931 |
+
"fix": {
|
| 932 |
+
"all": [
|
| 933 |
+
"No action needed now: this opt-in scan is planned for a later stage of the tool."
|
| 934 |
+
]
|
| 935 |
+
},
|
| 936 |
"verify": "Not applicable in stage 1."
|
| 937 |
},
|
|
|
|
| 938 |
"aiact.inventory": {
|
| 939 |
"why": "Deployers must know which AI systems they use before they can classify and govern them.",
|
| 940 |
+
"risk": {
|
| 941 |
+
"level": "Low",
|
| 942 |
+
"reason": "The inventory itself is information; the risk lies in unapproved or unclassified tools."
|
| 943 |
+
},
|
| 944 |
+
"fix": {
|
| 945 |
+
"all": [
|
| 946 |
+
"Add the AI tools found on this device to the organisation's AI register, with owner and purpose."
|
| 947 |
+
]
|
| 948 |
+
},
|
| 949 |
"verify": "The AI register lists every tool in the inventory."
|
| 950 |
},
|
| 951 |
"aiact.unapproved": {
|
| 952 |
"why": "AI tools used without approval may send work and personal data to providers without contracts or a transfer basis.",
|
| 953 |
+
"risk": {
|
| 954 |
+
"level": "Medium",
|
| 955 |
+
"reason": "Shadow AI can leak confidential or personal data and creates unassessed AI Act and GDPR obligations."
|
| 956 |
+
},
|
| 957 |
"fix": {
|
| 958 |
+
"all": [
|
| 959 |
+
"Decide for each listed tool: approve it (add to APPROVED_AI_TOOLS after a data protection and AI Act review, with a data processing agreement where needed) or remove it.",
|
| 960 |
+
"Publish an AI acceptable-use policy so staff know which tools are allowed."
|
| 961 |
+
],
|
| 962 |
+
"windows": [
|
| 963 |
+
"Remove unapproved apps (Settings > Apps) or block them with AppLocker / Intune app control."
|
| 964 |
+
],
|
| 965 |
+
"linux": [
|
| 966 |
+
"Remove unapproved tools with the package manager and restrict installation rights."
|
| 967 |
+
],
|
| 968 |
+
"macos": [
|
| 969 |
+
"Remove unapproved apps and restrict installations through your MDM."
|
| 970 |
+
]
|
| 971 |
},
|
| 972 |
"verify": "Re-run the audit; Unapproved AI tools should read none."
|
| 973 |
},
|
| 974 |
"aiact.risk_classification": {
|
| 975 |
"why": "Each AI system must be checked against the banned practices and the high-risk uses in Annex III.",
|
| 976 |
+
"risk": {
|
| 977 |
+
"level": "Medium",
|
| 978 |
+
"reason": "An unclassified tool could be used for a high-risk or prohibited purpose without the required safeguards."
|
| 979 |
+
},
|
| 980 |
+
"fix": {
|
| 981 |
+
"all": [
|
| 982 |
+
"Record the purpose for which each listed tool is used and classify it (prohibited, high-risk, limited-risk transparency, minimal). The auditor model does this from the inventory and the stated purpose."
|
| 983 |
+
]
|
| 984 |
+
},
|
| 985 |
"verify": "Every AI tool in the inventory has a classification in the AI register."
|
| 986 |
},
|
| 987 |
"aiact.os_ai_features": {
|
| 988 |
"why": "Built-in OS AI features can capture screen content or send text to external AI services.",
|
| 989 |
+
"risk": {
|
| 990 |
+
"level": "Medium",
|
| 991 |
+
"reason": "Screen snapshots or prompts can contain personal and confidential data processed without a defined purpose."
|
| 992 |
+
},
|
| 993 |
"fix": {
|
| 994 |
+
"windows": [
|
| 995 |
+
"Set Group Policy: Administrative Templates > Windows Components > Windows AI > Turn off saving snapshots for use with Recall = Enabled (DisableAIDataAnalysis = 1), unless Recall has been assessed and approved."
|
| 996 |
+
],
|
| 997 |
+
"macos": [
|
| 998 |
+
"Deploy a Restrictions profile with allowExternalIntelligenceIntegrations = false (and restrict other Apple Intelligence features your policy does not allow)."
|
| 999 |
+
],
|
| 1000 |
+
"linux": [
|
| 1001 |
+
"No action needed: Linux distributions have no built-in AI assistant."
|
| 1002 |
+
],
|
| 1003 |
+
"ios": [
|
| 1004 |
+
"Settings › Apple Intelligence & Siri › ChatGPT: turn off unless the organisation has approved it; the employer can restrict it with a configuration profile."
|
| 1005 |
+
],
|
| 1006 |
+
"android": [
|
| 1007 |
+
"In the Gemini app (or other assistant) settings, turn off screen context / access to app content unless the organisation has approved it."
|
| 1008 |
+
]
|
| 1009 |
},
|
| 1010 |
"verify": "Re-run the audit; the OS AI features check should be Compliant or NotApplicable."
|
| 1011 |
},
|
| 1012 |
"aiact.log_retention": {
|
| 1013 |
"why": "Deployers of high-risk AI systems must keep the system's logs for at least six months.",
|
| 1014 |
+
"risk": {
|
| 1015 |
+
"level": "Medium",
|
| 1016 |
+
"reason": "Missing logs prevent monitoring and investigation of high-risk AI use."
|
| 1017 |
+
},
|
| 1018 |
+
"fix": {
|
| 1019 |
+
"all": [
|
| 1020 |
+
"If any listed tool is classified high-risk, keep its automatically generated logs under your control for at least six months."
|
| 1021 |
+
]
|
| 1022 |
+
},
|
| 1023 |
"verify": "Log retention is documented for each high-risk AI system."
|
| 1024 |
},
|
|
|
|
| 1025 |
"org.ropa": {
|
| 1026 |
"why": "The record of processing activities is the core GDPR accountability document.",
|
| 1027 |
+
"risk": {
|
| 1028 |
+
"level": "High",
|
| 1029 |
+
"reason": "A missing or outdated record is a direct infringement and makes every other assessment harder."
|
| 1030 |
+
},
|
| 1031 |
+
"fix": {
|
| 1032 |
+
"all": [
|
| 1033 |
+
"Create or update the record of processing activities (purposes, categories of data and people, recipients, transfers, retention, security measures).",
|
| 1034 |
+
"Assign an owner and review it at least yearly."
|
| 1035 |
+
]
|
| 1036 |
+
},
|
| 1037 |
"verify": "Answer ORG_ROPA=yes in organisational.conf once the record is current."
|
| 1038 |
},
|
| 1039 |
"org.dpia": {
|
| 1040 |
"why": "High-risk processing needs a data protection impact assessment before it starts.",
|
| 1041 |
+
"risk": {
|
| 1042 |
+
"level": "Medium",
|
| 1043 |
+
"reason": "High-risk processing without a DPIA breaches GDPR and leaves risks to people unassessed."
|
| 1044 |
+
},
|
| 1045 |
+
"fix": {
|
| 1046 |
+
"all": [
|
| 1047 |
+
"List processing that is likely high-risk (e.g. monitoring employees, large-scale sensitive data, new technologies such as AI) and complete a DPIA for each."
|
| 1048 |
+
]
|
| 1049 |
+
},
|
| 1050 |
"verify": "Answer ORG_DPIA=yes once DPIAs are done."
|
| 1051 |
},
|
| 1052 |
"org.breach_procedure": {
|
| 1053 |
"why": "Breaches must be reported to the supervisory authority within 72 hours where required.",
|
| 1054 |
+
"risk": {
|
| 1055 |
+
"level": "High",
|
| 1056 |
+
"reason": "Without a tested procedure, the 72-hour deadline is easily missed."
|
| 1057 |
+
},
|
| 1058 |
+
"fix": {
|
| 1059 |
+
"all": [
|
| 1060 |
+
"Write a breach procedure (detection, assessment, 72-hour notification to the CNPD or your authority, informing people, breach log) and test it with an exercise."
|
| 1061 |
+
]
|
| 1062 |
+
},
|
| 1063 |
"verify": "Answer ORG_BREACH_PROCEDURE=yes once the procedure is written and tested."
|
| 1064 |
},
|
| 1065 |
"org.processor_agreements": {
|
| 1066 |
"why": "Every processor, including cloud and AI providers, needs a contract with the GDPR Article 28 terms.",
|
| 1067 |
+
"risk": {
|
| 1068 |
+
"level": "High",
|
| 1069 |
+
"reason": "Sharing personal data with a processor without an agreement breaches GDPR."
|
| 1070 |
+
},
|
| 1071 |
+
"fix": {
|
| 1072 |
+
"all": [
|
| 1073 |
+
"List all processors (including AI services used by staff) and sign a data processing agreement with each; check the transfer basis for non-EEA providers."
|
| 1074 |
+
]
|
| 1075 |
+
},
|
| 1076 |
"verify": "Answer ORG_PROCESSOR_AGREEMENTS=yes once all agreements are in place."
|
| 1077 |
},
|
| 1078 |
"org.ai_register": {
|
| 1079 |
"why": "A register of AI systems is the basis for AI Act classification and deployer duties.",
|
| 1080 |
+
"risk": {
|
| 1081 |
+
"level": "Medium",
|
| 1082 |
+
"reason": "Without a register, high-risk or prohibited AI use can go unnoticed."
|
| 1083 |
+
},
|
| 1084 |
+
"fix": {
|
| 1085 |
+
"all": [
|
| 1086 |
+
"Create an AI register listing each AI system, owner, purpose, risk class and the data it uses; use the AI software inventory from this audit as input."
|
| 1087 |
+
]
|
| 1088 |
+
},
|
| 1089 |
"verify": "Answer ORG_AI_REGISTER=yes once the register exists."
|
| 1090 |
},
|
| 1091 |
"org.ai_literacy": {
|
| 1092 |
"why": "Since 2 February 2025, organisations must ensure staff using AI have sufficient AI literacy.",
|
| 1093 |
+
"risk": {
|
| 1094 |
+
"level": "Medium",
|
| 1095 |
+
"reason": "Untrained staff misuse AI tools and share data they should not."
|
| 1096 |
+
},
|
| 1097 |
+
"fix": {
|
| 1098 |
+
"all": [
|
| 1099 |
+
"Provide AI literacy training matched to how staff use AI (capabilities, limits, data protection, the AI acceptable-use policy) and keep records."
|
| 1100 |
+
]
|
| 1101 |
+
},
|
| 1102 |
"verify": "Answer ORG_AI_LITERACY=yes once training is delivered and recorded."
|
| 1103 |
},
|
| 1104 |
"org.human_oversight": {
|
| 1105 |
"why": "High-risk AI systems need trained people who can understand, monitor and override them.",
|
| 1106 |
+
"risk": {
|
| 1107 |
+
"level": "Medium",
|
| 1108 |
+
"reason": "High-risk AI without oversight can harm people without anyone noticing."
|
| 1109 |
+
},
|
| 1110 |
+
"fix": {
|
| 1111 |
+
"all": [
|
| 1112 |
+
"For each high-risk AI system, name a trained person responsible for oversight; answer n/a if no high-risk AI is used."
|
| 1113 |
+
]
|
| 1114 |
+
},
|
| 1115 |
"verify": "Answer ORG_HUMAN_OVERSIGHT=yes (or n/a) in organisational.conf."
|
| 1116 |
},
|
| 1117 |
"org.ai_transparency": {
|
| 1118 |
"why": "People must know when they deal with AI or AI-generated content.",
|
| 1119 |
+
"risk": {
|
| 1120 |
+
"level": "Low",
|
| 1121 |
+
"reason": "Undisclosed AI use damages trust and breaches the AI Act transparency rules."
|
| 1122 |
+
},
|
| 1123 |
+
"fix": {
|
| 1124 |
+
"all": [
|
| 1125 |
+
"Set rules for labelling AI chatbots and AI-generated or manipulated content, and include them in the AI acceptable-use policy."
|
| 1126 |
+
]
|
| 1127 |
+
},
|
| 1128 |
"verify": "Answer ORG_AI_TRANSPARENCY=yes once the rules are in place."
|
| 1129 |
+
},
|
| 1130 |
+
"mobile.passcode_strength": {
|
| 1131 |
+
"why": "A short numeric code or a pattern can be guessed or seen over the user's shoulder; a longer code protects the data if the phone is lost.",
|
| 1132 |
+
"risk": {
|
| 1133 |
+
"level": "Medium",
|
| 1134 |
+
"reason": "A weak passcode lets someone who has the phone unlock it and read work data."
|
| 1135 |
+
},
|
| 1136 |
+
"fix": {
|
| 1137 |
+
"ios": [
|
| 1138 |
+
"Settings › Face ID & Passcode › Change Passcode › Passcode Options: choose a 6-digit, custom numeric (7+ digits) or alphanumeric code."
|
| 1139 |
+
],
|
| 1140 |
+
"android": [
|
| 1141 |
+
"Settings › Security & privacy › Device unlock › Screen lock: choose a PIN of at least {MOBILE_PASSCODE_MIN_DIGITS} digits or a password instead of a pattern."
|
| 1142 |
+
]
|
| 1143 |
+
},
|
| 1144 |
+
"verify": "Re-run the phone check; the passcode answer should be at least {MOBILE_PASSCODE_MIN_DIGITS} digits or alphanumeric."
|
| 1145 |
+
},
|
| 1146 |
+
"mobile.jailbreak_root": {
|
| 1147 |
+
"why": "Jailbreaking or rooting removes the operating system's app isolation and security checks.",
|
| 1148 |
+
"risk": {
|
| 1149 |
+
"level": "High",
|
| 1150 |
+
"reason": "Any app can then read other apps' data, including work email and files."
|
| 1151 |
+
},
|
| 1152 |
+
"fix": {
|
| 1153 |
+
"ios": [
|
| 1154 |
+
"Restore the iPhone with the latest iOS (Settings › General › Transfer or Reset iPhone › Erase All Content and Settings, or restore through a computer), then restore data from a backup."
|
| 1155 |
+
],
|
| 1156 |
+
"android": [
|
| 1157 |
+
"Remove root and re-lock the bootloader by installing the phone maker's official firmware, or stop using the phone for work."
|
| 1158 |
+
]
|
| 1159 |
+
},
|
| 1160 |
+
"verify": "Re-run the phone check after restoring; the phone should be reported as not jailbroken / not rooted."
|
| 1161 |
+
},
|
| 1162 |
+
"mobile.app_sources": {
|
| 1163 |
+
"why": "Official app stores review apps for malware; apps from other sources skip those checks.",
|
| 1164 |
+
"risk": {
|
| 1165 |
+
"level": "High",
|
| 1166 |
+
"reason": "Side-loaded apps are a common way malware reaches phones."
|
| 1167 |
+
},
|
| 1168 |
+
"fix": {
|
| 1169 |
+
"ios": [
|
| 1170 |
+
"Remove apps from alternative marketplaces or websites, and remove unknown profiles in Settings › General › VPN & Device Management; install work apps only from the App Store or the employer."
|
| 1171 |
+
],
|
| 1172 |
+
"android": [
|
| 1173 |
+
"Settings › Apps › Special app access › Install unknown apps: set every app to Not allowed; uninstall apps that did not come from Google Play."
|
| 1174 |
+
]
|
| 1175 |
+
},
|
| 1176 |
+
"verify": "Re-run the phone check; apps should come only from the official store (or the employer)."
|
| 1177 |
+
},
|
| 1178 |
+
"mobile.usb_debugging": {
|
| 1179 |
+
"why": "USB debugging lets a connected computer install apps and read data without the usual checks.",
|
| 1180 |
+
"risk": {
|
| 1181 |
+
"level": "Medium",
|
| 1182 |
+
"reason": "Anyone who can plug the unlocked phone into a computer can copy data or install software."
|
| 1183 |
+
},
|
| 1184 |
+
"fix": {
|
| 1185 |
+
"android": [
|
| 1186 |
+
"Settings › System › Developer options: turn off USB debugging, or turn off Developer options completely."
|
| 1187 |
+
]
|
| 1188 |
+
},
|
| 1189 |
+
"verify": "Re-run the phone check; USB debugging should be off."
|
| 1190 |
+
},
|
| 1191 |
+
"mobile.find_my_device": {
|
| 1192 |
+
"why": "Locating, locking and erasing a lost phone limits a personal data breach.",
|
| 1193 |
+
"risk": {
|
| 1194 |
+
"level": "Medium",
|
| 1195 |
+
"reason": "A lost phone cannot be wiped, so its data may have to be reported as a breach (GDPR Art. 33)."
|
| 1196 |
+
},
|
| 1197 |
+
"fix": {
|
| 1198 |
+
"ios": [
|
| 1199 |
+
"Settings › [your name] › Find My › Find My iPhone: turn on, together with Send Last Location."
|
| 1200 |
+
],
|
| 1201 |
+
"android": [
|
| 1202 |
+
"Settings › Google › All services › Find My Device (Find Hub): turn on, and make sure Location is on."
|
| 1203 |
+
]
|
| 1204 |
+
},
|
| 1205 |
+
"verify": "Re-run the phone check; Find My should be on."
|
| 1206 |
+
},
|
| 1207 |
+
"gdpr.lock_screen_previews": {
|
| 1208 |
+
"why": "Previews show message and email content, often personal data, to anyone who sees the locked phone.",
|
| 1209 |
+
"risk": {
|
| 1210 |
+
"level": "Low",
|
| 1211 |
+
"reason": "Personal data in notifications can be read without unlocking the phone."
|
| 1212 |
+
},
|
| 1213 |
+
"fix": {
|
| 1214 |
+
"ios": [
|
| 1215 |
+
"Settings › Notifications › Show Previews: choose When Unlocked or Never."
|
| 1216 |
+
],
|
| 1217 |
+
"android": [
|
| 1218 |
+
"Settings › Notifications › Notifications on lock screen: choose Hide sensitive content or Don't show notifications."
|
| 1219 |
+
]
|
| 1220 |
+
},
|
| 1221 |
+
"verify": "Re-run the phone check; previews should be hidden when the phone is locked."
|
| 1222 |
}
|
| 1223 |
}
|
| 1224 |
}
|
mobile/app.js
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Devseis Phone Check: browser detection + guided questions -> evidence (mobile-core.js) -> findings written by
|
| 2 |
+
// the auditor model in WebLLM (or the built-in writer) -> report the user can save or share as PDF.
|
| 3 |
+
// Nothing about the phone leaves the browser; only the model and the vulnerability data are downloaded.
|
| 4 |
+
import { Library, findingPrompt, findingTemplate, summaryPrompt, summaryTemplate, aiPrompt, aiTemplate, writeAnswer } from '../app/renderer/auditor-core.js';
|
| 5 |
+
import { buildReportHtml } from '../app/renderer/report.js';
|
| 6 |
+
import { loadModelWith } from '../app/renderer/model-core.js';
|
| 7 |
+
import { buildMobileEvidence, questionsFor } from './mobile-core.js';
|
| 8 |
+
import { detectDevice } from './detect.js';
|
| 9 |
+
import { loadPhoneVulnData } from './vuln-web.js';
|
| 10 |
+
|
| 11 |
+
const WEBLLM_URL = 'https://esm.run/@mlc-ai/web-llm@0.2.85';
|
| 12 |
+
const HTML2PDF_URL = 'https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js';
|
| 13 |
+
const MODEL_SIZE = 'about 350 MB, downloaded once';
|
| 14 |
+
|
| 15 |
+
const $ = id => document.getElementById(id);
|
| 16 |
+
const show = id => { document.querySelectorAll('.screen').forEach(s => s.classList.add('hidden')); $(`s-${id}`).classList.remove('hidden'); window.scrollTo(0, 0); };
|
| 17 |
+
const setBar = (id, fraction) => { $(id).style.width = `${Math.round(Math.max(0, Math.min(1, fraction)) * 100)}%`; };
|
| 18 |
+
const esc = value => String(value ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
| 19 |
+
|
| 20 |
+
const state = { device: null, data: null, lib: null, steps: [], index: 0, answers: {}, aiApps: [], version: null, versionEdited: false,
|
| 21 |
+
model: null, modelPromise: null, modelStatus: '', vulnPromise: null, report: null };
|
| 22 |
+
|
| 23 |
+
function readConf(text) {
|
| 24 |
+
const conf = {};
|
| 25 |
+
for (const line of text.split('\n')) {
|
| 26 |
+
if (!line.includes('=') || line.trimStart().startsWith('#')) continue;
|
| 27 |
+
const i = line.indexOf('=');
|
| 28 |
+
conf[line.slice(0, i).trim()] = line.slice(i + 1).trim().replace(/^"|"$/g, '');
|
| 29 |
+
}
|
| 30 |
+
return conf;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// Organisation answers can be passed by the employer in the link: #org=<base64 JSON {ORG_ROPA: "yes", ...}>
|
| 34 |
+
function orgFromLink() {
|
| 35 |
+
const m = /[#&]org=([^&]+)/.exec(location.hash);
|
| 36 |
+
if (!m) return null;
|
| 37 |
+
try { return JSON.parse(atob(decodeURIComponent(m[1]).replace(/-/g, '+').replace(/_/g, '/'))); } catch { return null; }
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
async function loadData() {
|
| 41 |
+
const get = async (path, type = 'json') => { const r = await fetch(path); if (!r.ok) throw new Error(`${path}: HTTP ${r.status}`); return type === 'json' ? r.json() : r.text(); };
|
| 42 |
+
const [catalog, questions, controls, guidance, aiTools, baselineText] = await Promise.all([
|
| 43 |
+
get('catalog/checks.json'), get('catalog/mobile_questions.json'), get('library/controls.json'), get('library/check_guidance.json'),
|
| 44 |
+
get('library/ai_tools.json'), get('baseline.conf', 'text')]);
|
| 45 |
+
return { catalog, questions, baseline: readConf(baselineText), library: { catalog, controls, guidance, aiTools } };
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// --- 1. Start ---------------------------------------------------------------------------------
|
| 49 |
+
function renderDevice() {
|
| 50 |
+
const d = state.device;
|
| 51 |
+
const rows = [['Device', d.device_model || (d.os_family ? 'Phone' : 'Computer')],
|
| 52 |
+
['System', d.os_name ? `${d.os_name} ${d.os_version || '(version not visible)'}` : 'Not a phone'],
|
| 53 |
+
['Browser', d.browser],
|
| 54 |
+
['Screen lock', d.platform_authenticator === true ? 'Unlock method detected' : d.platform_authenticator === false ? 'Not detected (will ask)' : 'Unknown (will ask)'],
|
| 55 |
+
['AI on this device', d.webgpu ? 'Supported (WebGPU)' : 'Not supported; the built-in writer is used']];
|
| 56 |
+
$('device-card').innerHTML = rows.map(([k, v]) => `<b>${esc(k)}</b><span>${esc(v)}</span>`).join('');
|
| 57 |
+
$('desktop-note').classList.toggle('hidden', !d.desktop);
|
| 58 |
+
$('start').disabled = !d.os_family;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
function demo(osFamily) {
|
| 62 |
+
Object.assign(state.device, osFamily === 'ios'
|
| 63 |
+
? { os_family: 'ios', os_name: 'iOS', os_version: null, version_precise: false, device_model: 'iPhone (demo on a computer)', platform_authenticator: null }
|
| 64 |
+
: { os_family: 'android', os_name: 'Android', os_version: null, version_precise: false, device_model: 'Android phone (demo on a computer)', platform_authenticator: null });
|
| 65 |
+
renderDevice();
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
// --- 2. Approval --------------------------------------------------------------------------------
|
| 69 |
+
function showConsent() {
|
| 70 |
+
const { catalog } = state.data;
|
| 71 |
+
const os = state.device.os_family;
|
| 72 |
+
const checks = catalog.checks.filter(c => c.os.includes(os));
|
| 73 |
+
$('check-count').textContent = checks.length;
|
| 74 |
+
$('plan').innerHTML = checks.map(c => `<li>${esc((c.names && c.names[os]) || c.name)}</li>`).join('');
|
| 75 |
+
const ai = $('use-ai');
|
| 76 |
+
ai.disabled = !state.device.webgpu;
|
| 77 |
+
ai.checked = state.device.webgpu && !state.device.cellular;
|
| 78 |
+
$('ai-note').textContent = !state.device.webgpu
|
| 79 |
+
? 'This browser has no WebGPU, so the built-in report writer is used.'
|
| 80 |
+
: `Runs on this phone (${MODEL_SIZE}; Wi-Fi recommended). Off = built-in report writer.`;
|
| 81 |
+
show('consent');
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
function startBackgroundWork() {
|
| 85 |
+
state.vulnPromise = loadPhoneVulnData();
|
| 86 |
+
if ($('use-ai').checked) {
|
| 87 |
+
const chip = $('model-chip');
|
| 88 |
+
chip.classList.remove('hidden');
|
| 89 |
+
chip.textContent = 'AI auditor: starting download';
|
| 90 |
+
state.modelPromise = import(WEBLLM_URL)
|
| 91 |
+
.then(webllm => loadModelWith(webllm, p => { chip.textContent = `AI auditor: ${Math.round((p.progress || 0) * 100)}%`; }))
|
| 92 |
+
.then(model => { state.model = model; chip.textContent = 'AI auditor ready'; return model; })
|
| 93 |
+
.catch(error => { chip.textContent = 'AI unavailable: built-in writer'; state.modelStatus = error.message; return null; });
|
| 94 |
+
} else {
|
| 95 |
+
state.modelPromise = Promise.resolve(null);
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
// --- 3. Questions -------------------------------------------------------------------------------
|
| 100 |
+
function buildSteps() {
|
| 101 |
+
const { catalog, questions } = state.data;
|
| 102 |
+
const d = state.device;
|
| 103 |
+
const steps = [{ kind: 'version', ...questions.os_version[d.os_family] }];
|
| 104 |
+
for (const q of questionsFor(d.os_family, catalog, questions, { platform_authenticator: d.platform_authenticator })) steps.push({ kind: 'check', ...q });
|
| 105 |
+
steps.push({ kind: 'ai', name: 'AI software inventory', ...questions.ai_apps });
|
| 106 |
+
return steps;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
function renderStep() {
|
| 110 |
+
const step = state.steps[state.index];
|
| 111 |
+
const total = state.steps.length;
|
| 112 |
+
setBar('q-bar', state.index / total);
|
| 113 |
+
$('q-count').textContent = `Question ${state.index + 1} of ${total}`;
|
| 114 |
+
$('q-check').textContent = step.kind === 'version' ? 'System version' : step.name;
|
| 115 |
+
$('q-text').textContent = step.question;
|
| 116 |
+
$('q-where').textContent = step.where;
|
| 117 |
+
const box = $('q-answers');
|
| 118 |
+
box.innerHTML = '';
|
| 119 |
+
const next = () => { state.index += 1; state.index < total ? renderStep() : finish(); };
|
| 120 |
+
|
| 121 |
+
if (step.kind === 'version') {
|
| 122 |
+
const input = document.createElement('input');
|
| 123 |
+
input.type = 'text'; input.inputMode = 'decimal'; input.placeholder = state.device.os_family === 'ios' ? 'for example 27.0.1' : 'for example 15';
|
| 124 |
+
input.value = state.version || state.device.os_version || '';
|
| 125 |
+
const hint = document.createElement('p');
|
| 126 |
+
hint.className = 'muted';
|
| 127 |
+
hint.textContent = state.device.os_version ? `Detected by the browser: ${state.device.os_version}. Correct it if Settings shows more detail.` : 'The browser could not see the version; please type it.';
|
| 128 |
+
const ok = button('Continue', 'primary', () => {
|
| 129 |
+
const value = input.value.trim();
|
| 130 |
+
if (!/^\d+(\.\d+){0,2}$/.test(value)) { input.focus(); return; }
|
| 131 |
+
state.version = value;
|
| 132 |
+
state.versionEdited = value !== state.device.os_version;
|
| 133 |
+
next();
|
| 134 |
+
});
|
| 135 |
+
box.append(hint, input, ok);
|
| 136 |
+
return;
|
| 137 |
+
}
|
| 138 |
+
if (step.kind === 'ai') {
|
| 139 |
+
for (const app of step.options) {
|
| 140 |
+
const label = document.createElement('label');
|
| 141 |
+
label.className = 'multi';
|
| 142 |
+
label.innerHTML = `<input type="checkbox" value="${esc(app)}" ${state.aiApps.includes(app) ? 'checked' : ''}><span>${esc(app)}</span>`;
|
| 143 |
+
box.append(label);
|
| 144 |
+
}
|
| 145 |
+
box.append(button('Continue', 'primary', () => {
|
| 146 |
+
state.aiApps = [...box.querySelectorAll('input:checked')].map(i => i.value);
|
| 147 |
+
next();
|
| 148 |
+
}));
|
| 149 |
+
return;
|
| 150 |
+
}
|
| 151 |
+
if (step.input === 'month') {
|
| 152 |
+
const input = document.createElement('input');
|
| 153 |
+
input.type = 'month';
|
| 154 |
+
input.value = typeof state.answers[step.id] === 'string' && state.answers[step.id] !== 'not_sure' ? state.answers[step.id] : '';
|
| 155 |
+
box.append(input, button('Continue', 'primary', () => { if (!/^\d{4}-\d{2}$/.test(input.value)) { input.focus(); return; } state.answers[step.id] = input.value; next(); }));
|
| 156 |
+
} else {
|
| 157 |
+
step.answers.forEach((answer, i) => box.append(button(answer.label, 'secondary', () => { state.answers[step.id] = i; next(); })));
|
| 158 |
+
}
|
| 159 |
+
box.append(button(state.data.questions.not_sure.label, 'secondary not-sure', () => { state.answers[step.id] = 'not_sure'; next(); }));
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
function button(text, className, onClick) {
|
| 163 |
+
const b = document.createElement('button');
|
| 164 |
+
b.className = className; b.textContent = text; b.type = 'button';
|
| 165 |
+
b.addEventListener('click', onClick);
|
| 166 |
+
return b;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
// --- 4. Evidence and report -----------------------------------------------------------------------
|
| 170 |
+
async function finish() {
|
| 171 |
+
show('work');
|
| 172 |
+
const d = state.device;
|
| 173 |
+
$('w-step').textContent = 'Checking the vulnerability data';
|
| 174 |
+
setBar('w-bar', 0.02);
|
| 175 |
+
const vuln = await state.vulnPromise;
|
| 176 |
+
const now = new Date();
|
| 177 |
+
const offset = -now.getTimezoneOffset();
|
| 178 |
+
const pad = n => String(Math.abs(n)).padStart(2, '0');
|
| 179 |
+
const started = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}${offset >= 0 ? '+' : '-'}${pad(Math.floor(Math.abs(offset) / 60))}:${pad(Math.abs(offset) % 60)}`;
|
| 180 |
+
const input = {
|
| 181 |
+
os_family: d.os_family, os_name: d.os_name, os_version: state.version, version_source: state.versionEdited || !d.os_version ? 'user' : 'browser',
|
| 182 |
+
browser: d.browser, device_model: d.device_model || (d.os_family === 'ios' ? 'iPhone' : 'Android phone'), hostname: d.device_model || d.os_name,
|
| 183 |
+
started_at: started, platform_authenticator: d.platform_authenticator, answers: state.answers, ai_apps: state.aiApps, org: orgFromLink(),
|
| 184 |
+
};
|
| 185 |
+
const evidence = buildMobileEvidence(input, { catalog: state.data.catalog, questions: state.data.questions, baseline: state.data.baseline, vuln, now: now.getTime() });
|
| 186 |
+
const evidenceText = JSON.stringify(evidence, null, 2);
|
| 187 |
+
const sha256 = [...new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(evidenceText)))].map(b => b.toString(16).padStart(2, '0')).join('');
|
| 188 |
+
|
| 189 |
+
if (state.modelPromise && !state.model) {
|
| 190 |
+
$('w-title').textContent = 'Getting the AI auditor ready';
|
| 191 |
+
$('w-step').textContent = 'Finishing the model download (only the first time)…';
|
| 192 |
+
await state.modelPromise;
|
| 193 |
+
}
|
| 194 |
+
const model = state.model;
|
| 195 |
+
const lib = state.lib;
|
| 196 |
+
const targets = evidence.results.filter(r => ['Non-Compliant', 'Error', 'Pending'].includes(r.status) && r.section !== 'org');
|
| 197 |
+
const ai = aiPrompt(evidence, lib);
|
| 198 |
+
const total = targets.length + 1 + (ai ? 1 : 0);
|
| 199 |
+
let done = 0;
|
| 200 |
+
const sources = { model: 0, template: 0 };
|
| 201 |
+
const generate = model ? model.generate : null;
|
| 202 |
+
const tick = label => { done += 1; setBar('w-bar', done / total); $('w-current').textContent = label; };
|
| 203 |
+
$('w-title').textContent = 'Writing your report';
|
| 204 |
+
$('w-step').textContent = model ? `The AI auditor is writing ${total} parts on this phone` : 'The built-in writer is writing the findings';
|
| 205 |
+
|
| 206 |
+
const findings = {};
|
| 207 |
+
for (const r of evidence.results.filter(x => x.section === 'org' && x.status !== 'Compliant')) {
|
| 208 |
+
findings[r.id] = findingTemplate(evidence, r, lib); // organisational answers: short, fixed wording
|
| 209 |
+
}
|
| 210 |
+
for (const r of targets) {
|
| 211 |
+
$('w-current').textContent = `Writing: ${r.name}`;
|
| 212 |
+
const out = await writeAnswer('finding', findingPrompt(evidence, r, lib), findingTemplate(evidence, r, lib), generate);
|
| 213 |
+
findings[r.id] = out.answer;
|
| 214 |
+
sources[out.source] += 1;
|
| 215 |
+
tick(`Written: ${r.name}`);
|
| 216 |
+
}
|
| 217 |
+
const summaryBase = summaryTemplate(evidence, lib);
|
| 218 |
+
const summaryOut = await writeAnswer('summary', summaryPrompt(evidence, lib), summaryBase, generate);
|
| 219 |
+
const summary = summaryOut.source === 'model' ? { ...summaryBase, summary: summaryOut.answer.summary, next_steps: summaryOut.answer.next_steps } : summaryBase;
|
| 220 |
+
sources[summaryOut.source] += 1;
|
| 221 |
+
tick('Written: summary');
|
| 222 |
+
let aiResult = null;
|
| 223 |
+
if (ai) {
|
| 224 |
+
const out = await writeAnswer('ai_classification', ai, aiTemplate(evidence, lib), generate);
|
| 225 |
+
aiResult = out.answer;
|
| 226 |
+
sources[out.source] += 1;
|
| 227 |
+
tick('Written: AI app classification');
|
| 228 |
+
}
|
| 229 |
+
const html = buildReportHtml({ evidence, findings, summary, ai: aiResult, sha256, model: model ? model.name : null, sources });
|
| 230 |
+
state.report = { html, evidence, evidenceText, summary, sources };
|
| 231 |
+
showResult();
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
// --- 5. Result, PDF and sharing ----------------------------------------------------------------
|
| 235 |
+
function showResult() {
|
| 236 |
+
const { evidence, summary, sources } = state.report;
|
| 237 |
+
const pills = Object.entries(evidence.summary).filter(([, v]) => v).map(([k, v]) => `<span class="pill s-${k}">${v} ${k}</span>`).join('');
|
| 238 |
+
const gaps = summary.top_gaps.map(g => `<li><b class="s-Non-Compliant">${esc(g.risk)}</b> — ${esc(g.name)}: ${esc(g.found)}</li>`).join('');
|
| 239 |
+
const reported = evidence.results.filter(r => r.evidence === 'self-reported').length;
|
| 240 |
+
const detected = evidence.results.filter(r => r.evidence === 'detected').length;
|
| 241 |
+
$('result').innerHTML = `
|
| 242 |
+
<h1>Your phone check</h1>
|
| 243 |
+
<div class="card"><div class="score">${summary.score.percent}%</div>
|
| 244 |
+
<p class="muted">${summary.score.compliant} of ${summary.score.assessed} assessed checks compliant · ${detected} detected, ${reported} self-reported</p>
|
| 245 |
+
<div class="pills">${pills}</div>
|
| 246 |
+
<p>${esc(summary.summary)}</p>
|
| 247 |
+
${gaps ? `<b>Top gaps</b><ul class="gaps">${gaps}</ul>` : ''}
|
| 248 |
+
<p class="muted">Written ${sources.model ? `by the AI auditor (${sources.model}) and ` : ''}by the built-in writer (${sources.template}).</p>
|
| 249 |
+
</div>`;
|
| 250 |
+
$('pdf-note').textContent = 'The report stays on this phone until you save or share it.';
|
| 251 |
+
show('done');
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
function loadScript(src) {
|
| 255 |
+
return new Promise((resolve, reject) => {
|
| 256 |
+
if ([...document.scripts].some(s => s.src === src)) return resolve();
|
| 257 |
+
const s = document.createElement('script');
|
| 258 |
+
s.src = src; s.onload = resolve; s.onerror = () => reject(new Error('could not load the PDF tool'));
|
| 259 |
+
document.head.append(s);
|
| 260 |
+
});
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
async function makePdf() {
|
| 264 |
+
await loadScript(HTML2PDF_URL);
|
| 265 |
+
const doc = new DOMParser().parseFromString(state.report.html, 'text/html');
|
| 266 |
+
const holder = document.createElement('div');
|
| 267 |
+
holder.style.cssText = 'position:fixed;left:-10000px;top:0;width:794px;background:#fff;color:#16181d';
|
| 268 |
+
const style = document.createElement('style');
|
| 269 |
+
style.textContent = doc.querySelector('style').textContent.replace(/(^|\n)\s*body\s*\{/g, '$1.devseis-report {');
|
| 270 |
+
const body = document.createElement('div');
|
| 271 |
+
body.className = 'devseis-report';
|
| 272 |
+
body.innerHTML = doc.body.innerHTML;
|
| 273 |
+
holder.append(style, body);
|
| 274 |
+
document.body.append(holder);
|
| 275 |
+
try {
|
| 276 |
+
return await window.html2pdf().set({ margin: 10, image: { type: 'jpeg', quality: 0.92 }, html2canvas: { scale: 1.5, useCORS: true },
|
| 277 |
+
jsPDF: { unit: 'mm', format: 'a4' }, pagebreak: { mode: ['css', 'legacy'] } }).from(body).outputPdf('blob');
|
| 278 |
+
} finally {
|
| 279 |
+
holder.remove();
|
| 280 |
+
}
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
function fileName(ext) {
|
| 284 |
+
const e = state.report.evidence;
|
| 285 |
+
return `devseis-phone-check-${e.host.os_family}-${e.audit.started_at.slice(0, 10)}.${ext}`;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
function download(blob, name) {
|
| 289 |
+
const a = document.createElement('a');
|
| 290 |
+
a.href = URL.createObjectURL(blob); a.download = name;
|
| 291 |
+
document.body.append(a); a.click(); a.remove();
|
| 292 |
+
setTimeout(() => URL.revokeObjectURL(a.href), 30000);
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
async function sharePdf() {
|
| 296 |
+
$('pdf-note').textContent = 'Preparing the PDF…';
|
| 297 |
+
try {
|
| 298 |
+
const blob = await makePdf();
|
| 299 |
+
const file = new File([blob], fileName('pdf'), { type: 'application/pdf' });
|
| 300 |
+
if (navigator.canShare && navigator.canShare({ files: [file] })) {
|
| 301 |
+
try {
|
| 302 |
+
await navigator.share({ files: [file], title: 'Devseis phone check' });
|
| 303 |
+
$('pdf-note').textContent = 'Shared. Choose "Save to Files" in the share sheet to keep a copy on the phone.';
|
| 304 |
+
return;
|
| 305 |
+
} catch (error) {
|
| 306 |
+
if (error.name === 'AbortError') { $('pdf-note').textContent = 'Sharing cancelled.'; return; }
|
| 307 |
+
}
|
| 308 |
+
}
|
| 309 |
+
download(blob, file.name);
|
| 310 |
+
$('pdf-note').textContent = `Saved as ${file.name} (Downloads).`;
|
| 311 |
+
} catch (error) {
|
| 312 |
+
$('pdf-note').textContent = `The PDF could not be made (${error.message}). Use "View full report" and your browser's Share › Print to save it as PDF.`;
|
| 313 |
+
}
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
function viewReport() {
|
| 317 |
+
const url = URL.createObjectURL(new Blob([state.report.html], { type: 'text/html' }));
|
| 318 |
+
window.open(url, '_blank');
|
| 319 |
+
setTimeout(() => URL.revokeObjectURL(url), 60000);
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
// --- Wiring ----------------------------------------------------------------------------------------
|
| 323 |
+
async function init() {
|
| 324 |
+
try {
|
| 325 |
+
[state.device, state.data] = await Promise.all([detectDevice(), loadData()]);
|
| 326 |
+
} catch (error) {
|
| 327 |
+
$('device-card').innerHTML = `<span class="s-Error">The check could not start: ${esc(error.message)}</span>`;
|
| 328 |
+
return;
|
| 329 |
+
}
|
| 330 |
+
state.lib = new Library(state.data.library);
|
| 331 |
+
renderDevice();
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
$('start').addEventListener('click', showConsent);
|
| 335 |
+
document.querySelectorAll('[data-demo]').forEach(b => b.addEventListener('click', () => demo(b.dataset.demo)));
|
| 336 |
+
$('consent-back').addEventListener('click', () => show('start'));
|
| 337 |
+
$('consent-ok').addEventListener('click', () => {
|
| 338 |
+
startBackgroundWork();
|
| 339 |
+
state.steps = buildSteps();
|
| 340 |
+
state.index = 0;
|
| 341 |
+
show('question');
|
| 342 |
+
renderStep();
|
| 343 |
+
});
|
| 344 |
+
$('q-back').addEventListener('click', () => { if (state.index > 0) { state.index -= 1; renderStep(); } else show('consent'); });
|
| 345 |
+
$('share-pdf').addEventListener('click', sharePdf);
|
| 346 |
+
$('view-report').addEventListener('click', viewReport);
|
| 347 |
+
$('save-json').addEventListener('click', () => download(new Blob([state.report.evidenceText], { type: 'application/json' }), fileName('json')));
|
| 348 |
+
$('again').addEventListener('click', () => { state.answers = {}; state.aiApps = []; state.version = null; show('start'); });
|
| 349 |
+
init();
|
| 350 |
+
|
| 351 |
+
// Automated tests only (?test in the URL): expose state and the PDF maker.
|
| 352 |
+
if (new URLSearchParams(location.search).has('test')) Object.assign(window, { __devseisState: state, __devseisMakePdf: makePdf });
|
mobile/build-evidence.mjs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Build phone evidence records from answer sets (JSON lines on stdin -> evidence JSON lines on stdout),
|
| 2 |
+
// using exactly the code the mobile web page uses. Called by synthetic/generate_evidence.py.
|
| 3 |
+
// Usage: node mobile/build-evidence.mjs [--vulndb DIR] [--now ISO-DATE] < answers.jsonl > evidence.jsonl
|
| 4 |
+
import { readFileSync } from 'node:fs';
|
| 5 |
+
import { gunzipSync } from 'node:zlib';
|
| 6 |
+
import path from 'node:path';
|
| 7 |
+
import { fileURLToPath } from 'node:url';
|
| 8 |
+
import { buildMobileEvidence } from './mobile-core.js';
|
| 9 |
+
|
| 10 |
+
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
| 11 |
+
const arg = name => { const i = process.argv.indexOf(name); return i > 0 ? process.argv[i + 1] : null; };
|
| 12 |
+
const readConf = file => Object.fromEntries(readFileSync(file, 'utf8').split('\n')
|
| 13 |
+
.filter(l => l.includes('=') && !l.trimStart().startsWith('#'))
|
| 14 |
+
.map(l => { const i = l.indexOf('='); return [l.slice(0, i).trim(), l.slice(i + 1).trim().replace(/^"|"$/g, '')]; }));
|
| 15 |
+
|
| 16 |
+
const catalog = JSON.parse(readFileSync(path.join(root, 'catalog', 'checks.json'), 'utf8'));
|
| 17 |
+
const questions = JSON.parse(readFileSync(path.join(root, 'catalog', 'mobile_questions.json'), 'utf8'));
|
| 18 |
+
const baseline = readConf(arg('--baseline') || path.join(root, 'baseline.conf'));
|
| 19 |
+
let vuln = { error: 'no vulnerability data found' };
|
| 20 |
+
try {
|
| 21 |
+
const dir = arg('--vulndb') || path.join(root, 'vulndb', 'out');
|
| 22 |
+
const manifest = JSON.parse(readFileSync(path.join(dir, 'manifest.json'), 'utf8'));
|
| 23 |
+
vuln = { manifest, bundle: JSON.parse(gunzipSync(readFileSync(path.join(dir, manifest.mobile.file))).toString('utf8')) };
|
| 24 |
+
} catch (error) { vuln = { error: error.message }; }
|
| 25 |
+
const now = arg('--now') ? Date.parse(arg('--now')) : Date.now();
|
| 26 |
+
|
| 27 |
+
const lines = readFileSync(0, 'utf8').split('\n').filter(Boolean);
|
| 28 |
+
for (const line of lines) {
|
| 29 |
+
const input = JSON.parse(line);
|
| 30 |
+
const record = buildMobileEvidence(input, { catalog, questions, baseline, vuln, now: input.now ? Date.parse(input.now) : now });
|
| 31 |
+
if (input.synthetic) Object.assign(record.source, { kind: 'synthetic', ...input.synthetic });
|
| 32 |
+
process.stdout.write(JSON.stringify(record) + '\n');
|
| 33 |
+
}
|
mobile/detect.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// What a web page can learn about the phone it runs on. Browsers deliberately hide security settings,
|
| 2 |
+
// so this is limited to the OS and version, device model (Android), browser, whether a device unlock
|
| 3 |
+
// method is set up, and WebGPU support. Everything else is asked.
|
| 4 |
+
|
| 5 |
+
function iosVersion(ua) {
|
| 6 |
+
// Safari 26+ freezes the OS version in the user agent (it reports 18_6), but its own Version/ follows iOS.
|
| 7 |
+
const safari = /Version\/(\d+(?:\.\d+){0,2}).*Safari/.exec(ua);
|
| 8 |
+
const os = /OS (\d+)_(\d+)(?:_(\d+))?/.exec(ua);
|
| 9 |
+
const osVersion = os ? [os[1], os[2], os[3]].filter(Boolean).join('.') : null;
|
| 10 |
+
if (safari && !/CriOS|FxiOS|EdgiOS/.test(ua) && parseInt(safari[1], 10) >= 26) return { version: safari[1], precise: false };
|
| 11 |
+
if (osVersion && osVersion !== '18.6') return { version: osVersion, precise: Boolean(os[3]) };
|
| 12 |
+
if (safari) return { version: safari[1], precise: false };
|
| 13 |
+
return { version: osVersion, precise: false };
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
function browserName(ua, brands) {
|
| 17 |
+
let m;
|
| 18 |
+
if ((m = /SamsungBrowser\/(\d+(?:\.\d+)?)/.exec(ua))) return `Samsung Internet ${m[1]}`;
|
| 19 |
+
if ((m = /EdgA\/(\d+)|EdgiOS\/(\d+)/.exec(ua))) return `Edge ${m[1] || m[2]}`;
|
| 20 |
+
if ((m = /Firefox\/(\d+)|FxiOS\/(\d+)/.exec(ua))) return `Firefox ${m[1] || m[2]}`;
|
| 21 |
+
if ((m = /CriOS\/(\d+)/.exec(ua))) return `Chrome ${m[1]} (iOS)`;
|
| 22 |
+
const chrome = (brands || []).find(b => b.brand === 'Google Chrome');
|
| 23 |
+
if (chrome) return `Chrome ${chrome.version}`;
|
| 24 |
+
if ((m = /Chrome\/(\d+)/.exec(ua))) return `Chrome ${m[1]}`;
|
| 25 |
+
if ((m = /Version\/(\d+(?:\.\d+)?).*Safari/.exec(ua))) return `Safari ${m[1]}`;
|
| 26 |
+
return 'Unknown browser';
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
async function withTimeout(promise, ms, fallback) {
|
| 30 |
+
return Promise.race([promise, new Promise(resolve => setTimeout(() => resolve(fallback), ms))]);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
export async function detectDevice() {
|
| 34 |
+
const ua = navigator.userAgent;
|
| 35 |
+
const uaData = navigator.userAgentData;
|
| 36 |
+
const out = { os_family: null, os_name: null, os_version: null, version_precise: false, device_model: '', browser: browserName(ua, uaData && uaData.brands),
|
| 37 |
+
platform_authenticator: null, webgpu: 'gpu' in navigator, cellular: Boolean(navigator.connection && navigator.connection.type === 'cellular'), desktop: false };
|
| 38 |
+
|
| 39 |
+
const ipadDesktopMode = /Macintosh/.test(ua) && navigator.maxTouchPoints > 1;
|
| 40 |
+
if (/iPhone|iPod/.test(ua) || /iPad/.test(ua) || ipadDesktopMode) {
|
| 41 |
+
const ipad = /iPad/.test(ua) || ipadDesktopMode;
|
| 42 |
+
const v = iosVersion(ua);
|
| 43 |
+
Object.assign(out, { os_family: 'ios', os_name: ipad ? 'iPadOS' : 'iOS', os_version: v.version, version_precise: v.precise, device_model: ipad ? 'iPad' : 'iPhone' });
|
| 44 |
+
} else if (/Android/.test(ua)) {
|
| 45 |
+
out.os_family = 'android';
|
| 46 |
+
out.os_name = 'Android';
|
| 47 |
+
if (uaData && uaData.getHighEntropyValues) {
|
| 48 |
+
try {
|
| 49 |
+
const hi = await withTimeout(uaData.getHighEntropyValues(['platform', 'platformVersion', 'model']), 1500, {});
|
| 50 |
+
// Only trust the hints when they describe Android (desktop browsers emulating a phone report the host OS).
|
| 51 |
+
if (hi.platform === 'Android') {
|
| 52 |
+
if (hi.platformVersion) out.os_version = String(parseInt(hi.platformVersion, 10));
|
| 53 |
+
if (hi.model) out.device_model = hi.model;
|
| 54 |
+
}
|
| 55 |
+
} catch { /* not allowed */ }
|
| 56 |
+
}
|
| 57 |
+
if (!out.os_version) {
|
| 58 |
+
const m = /Android (\d+)/.exec(ua);
|
| 59 |
+
// Chrome's reduced user agent always says "Android 10; K": not a real version.
|
| 60 |
+
if (m && !/Android 10; K\)/.test(ua)) out.os_version = m[1];
|
| 61 |
+
}
|
| 62 |
+
if (!out.device_model) {
|
| 63 |
+
const m = /Android [\d.]+; ([^;)]+)[;)]/.exec(ua);
|
| 64 |
+
if (m && m[1] !== 'K') out.device_model = m[1].trim();
|
| 65 |
+
}
|
| 66 |
+
} else {
|
| 67 |
+
out.desktop = true;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
if (window.PublicKeyCredential && PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable) {
|
| 71 |
+
try {
|
| 72 |
+
out.platform_authenticator = await withTimeout(PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(), 2000, null);
|
| 73 |
+
} catch { out.platform_authenticator = null; }
|
| 74 |
+
}
|
| 75 |
+
return out;
|
| 76 |
+
}
|
mobile/mobile-core.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Phone self-check: turns what the browser detected and what the user answered into evidence in the
|
| 2 |
+
// same format as the desktop collectors (schema/evidence.schema.json, source.kind = "web").
|
| 3 |
+
// Used by the mobile web page and, through mobile/build-evidence.mjs, by the synthetic generator, so the
|
| 4 |
+
// training data uses exactly the wording the page produces. Pure functions only (no DOM, no network).
|
| 5 |
+
import { matchApps, knownVulnResult } from '../app/renderer/vuln-core.js';
|
| 6 |
+
|
| 7 |
+
export const MOBILE_VERSION = '0.3.0';
|
| 8 |
+
const MOBILE = new Set(['ios', 'android']);
|
| 9 |
+
const ORG_PENDING = 'Answered once by the organisation (organisational.conf); not part of the phone self-check.';
|
| 10 |
+
|
| 11 |
+
const major = version => parseInt(String(version).split('.')[0], 10) || 0;
|
| 12 |
+
const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
|
| 13 |
+
|
| 14 |
+
// Text and status for an answer whose result depends on the baseline.
|
| 15 |
+
function computed(spec, answer, B, os) {
|
| 16 |
+
if (spec.expected_from === 'seconds') {
|
| 17 |
+
const limit = parseInt(B.SCREEN_LOCK_MAX_SECONDS, 10);
|
| 18 |
+
const label = os === 'ios' ? 'Auto-Lock' : 'Screen timeout';
|
| 19 |
+
const ok = answer.seconds != null && answer.seconds > 0 && answer.seconds <= limit;
|
| 20 |
+
return { status: ok ? 'Compliant' : 'Non-Compliant', actual: `${label} ${answer.label}`, expected: `<= ${limit} seconds`,
|
| 21 |
+
detail: ok ? '' : answer.seconds == null ? 'The phone never locks by itself.' : `The phone stays unlocked for ${answer.label} when not in use.` };
|
| 22 |
+
}
|
| 23 |
+
if (spec.expected_from === 'passcode') {
|
| 24 |
+
const min = parseInt(B.MOBILE_PASSCODE_MIN_DIGITS, 10);
|
| 25 |
+
const expected = `>= ${min} digits or alphanumeric`;
|
| 26 |
+
if (answer.alphanumeric) return { status: 'Compliant', actual: 'alphanumeric', expected, detail: '' };
|
| 27 |
+
if (answer.pattern) return { status: 'Non-Compliant', actual: 'pattern', expected, detail: 'Pattern locks are easy to guess from screen smudges or by watching.' };
|
| 28 |
+
if (answer.none) return { status: 'Non-Compliant', actual: 'swipe or none', expected, detail: 'There is no secret needed to unlock the phone.' };
|
| 29 |
+
const ok = answer.digits >= min;
|
| 30 |
+
const shown = answer.digits >= 7 ? '7+ digit numeric code' : `${answer.digits}-digit ${os === 'android' ? 'PIN' : 'code'}`;
|
| 31 |
+
return { status: ok ? 'Compliant' : 'Non-Compliant', actual: shown, expected, detail: ok ? '' : `A ${answer.digits}-digit code has only ${10 ** answer.digits} combinations.` };
|
| 32 |
+
}
|
| 33 |
+
throw new Error(`unknown computed answer type ${spec.expected_from}`);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// Android security patch level ("YYYY-MM") against MOBILE_PATCH_MAX_AGE_DAYS.
|
| 37 |
+
function patchResult(value, B, now) {
|
| 38 |
+
const maxDays = parseInt(B.MOBILE_PATCH_MAX_AGE_DAYS, 10);
|
| 39 |
+
const expected = `<= ${maxDays} days old`;
|
| 40 |
+
const m = /^(\d{4})-(\d{2})/.exec(value || '');
|
| 41 |
+
if (!m) return null;
|
| 42 |
+
const days = Math.floor((now - Date.UTC(+m[1], +m[2] - 1, 1)) / 86400000);
|
| 43 |
+
const months = Math.max(0, Math.round(days / 30.4));
|
| 44 |
+
const ok = days <= maxDays;
|
| 45 |
+
return { status: ok ? 'Compliant' : 'Non-Compliant', actual: `security patch ${m[1]}-${m[2]} (${plural(months, 'month')} old)`, expected,
|
| 46 |
+
detail: ok ? '' : `Security fixes published since ${m[1]}-${m[2]} are missing; Google publishes Android fixes every month.` };
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// Newest fixed iOS version for the same major line, from the NVD ranges in the bundle.
|
| 50 |
+
function latestIosFix(bundle, product, version) {
|
| 51 |
+
const app = bundle.apps.find(a => a.id === product);
|
| 52 |
+
if (!app) return null;
|
| 53 |
+
let best = null;
|
| 54 |
+
for (const row of app.cves) {
|
| 55 |
+
const fixed = row[5];
|
| 56 |
+
if (fixed && major(fixed) === major(version) && (!best || cmp(fixed, best) > 0)) best = fixed;
|
| 57 |
+
}
|
| 58 |
+
return best;
|
| 59 |
+
}
|
| 60 |
+
// Apple fixes some vulnerabilities only in the newest major version.
|
| 61 |
+
function newerMajor(bundle, product, version) {
|
| 62 |
+
const app = bundle.apps.find(a => a.id === product);
|
| 63 |
+
let top = null;
|
| 64 |
+
for (const row of app ? app.cves : []) if (row[5] && major(row[5]) > major(version) && (!top || cmp(row[5], top) > 0)) top = row[5];
|
| 65 |
+
return top ? ` ${major(top)}.x (newest: ${top}) fixes vulnerabilities that are not fixed in ${major(version)}.x.` : '';
|
| 66 |
+
}
|
| 67 |
+
function cmp(a, b) {
|
| 68 |
+
const x = String(a).split('.').map(Number), y = String(b).split('.').map(Number);
|
| 69 |
+
for (let i = 0; i < Math.max(x.length, y.length); i++) { const d = (x[i] || 0) - (y[i] || 0); if (d) return d; }
|
| 70 |
+
return 0;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
function osSupported(input, B) {
|
| 74 |
+
const ios = input.os_family === 'ios';
|
| 75 |
+
const min = parseInt(ios ? B.IOS_MIN_MAJOR : B.ANDROID_MIN_MAJOR, 10);
|
| 76 |
+
const ok = major(input.os_version) >= min;
|
| 77 |
+
return { status: ok ? 'Compliant' : 'Non-Compliant', actual: `${input.os_name} ${input.os_version}`, expected: `>= ${ios ? 'iOS' : 'Android'} ${min}`,
|
| 78 |
+
detail: (input.version_source === 'user' ? `Version entered by the user from ${ios ? 'Settings › General › About' : 'Settings › About phone'}.` : `Version reported by the browser (${input.browser}).`)
|
| 79 |
+
+ (ok ? '' : ' This version no longer receives all security updates.') };
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
function aiResults(id, apps, B) {
|
| 83 |
+
const approved = (B.APPROVED_AI_TOOLS || '').split(',').map(a => a.trim().toLowerCase()).filter(Boolean);
|
| 84 |
+
if (id === 'aiact.inventory') return { status: 'Compliant', actual: apps.join(', ') || 'no AI tools found', expected: 'inventory taken', detail: 'AI apps listed by the user.' };
|
| 85 |
+
if (id === 'aiact.unapproved') {
|
| 86 |
+
const bad = apps.filter(a => !approved.includes(a.toLowerCase()));
|
| 87 |
+
return { status: bad.length ? 'Non-Compliant' : 'Compliant', actual: bad.join(', ') || 'none', expected: `only: ${B.APPROVED_AI_TOOLS || 'none'}`, detail: '' };
|
| 88 |
+
}
|
| 89 |
+
if (id === 'aiact.risk_classification') {
|
| 90 |
+
return apps.length ? { status: 'Pending', actual: apps.join(', '), expected: 'every AI tool classified', detail: 'Classified by the auditor model (Caveat classifier) in a later step.' }
|
| 91 |
+
: { status: 'NotApplicable', actual: 'no AI tools found', expected: 'every AI tool classified', detail: '' };
|
| 92 |
+
}
|
| 93 |
+
return apps.length ? { status: 'Pending', actual: 'depends on classification', expected: '>= 6 months for high-risk AI', detail: 'Required only if a tool is classified high-risk.' }
|
| 94 |
+
: { status: 'NotApplicable', actual: 'no AI tools found', expected: '>= 6 months for high-risk AI', detail: '' };
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
function orgResult(check, answer) {
|
| 98 |
+
if (answer === 'yes') return { status: 'Compliant', actual: 'yes', expected: 'yes', detail: '' };
|
| 99 |
+
if (answer === 'no' || answer === 'partial') return { status: 'Non-Compliant', actual: answer, expected: 'yes', detail: '' };
|
| 100 |
+
if (answer === 'n/a') return { status: 'NotApplicable', actual: 'n/a', expected: 'yes', detail: '' };
|
| 101 |
+
return { status: 'Pending', actual: 'not answered', expected: 'yes', detail: answer === undefined ? ORG_PENDING : `Answer ${check.answer_key} in organisational.conf.` };
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
// The questions the page shows for this OS, in catalog order (detected checks are skipped).
|
| 105 |
+
export function questionsFor(osFamily, catalog, questions, detected = {}) {
|
| 106 |
+
const list = [];
|
| 107 |
+
for (const check of catalog.checks) {
|
| 108 |
+
if (!check.os.includes(osFamily)) continue;
|
| 109 |
+
const spec = (questions.checks[check.id] || {})[osFamily];
|
| 110 |
+
if (!spec) continue;
|
| 111 |
+
if (check.id === 'screen_lock.enabled' && detected.platform_authenticator === true) continue;
|
| 112 |
+
list.push({ id: check.id, name: (check.names && check.names[osFamily]) || check.name, ...spec });
|
| 113 |
+
}
|
| 114 |
+
return list;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
/**
|
| 118 |
+
* input: { os_family, os_name, os_version, version_source: 'browser'|'user', browser, device_model, hostname, started_at,
|
| 119 |
+
* platform_authenticator: true|false|null, answers: { checkId: answerIndex | 'not_sure' | value }, ai_apps: [...], org: {key: answer}|null }
|
| 120 |
+
* ctx: { catalog, questions, baseline, vuln: {bundle, manifest}|null|{error}, now }
|
| 121 |
+
*/
|
| 122 |
+
export function buildMobileEvidence(input, ctx) {
|
| 123 |
+
const { catalog, questions, baseline: B } = ctx;
|
| 124 |
+
const now = ctx.now ?? Date.now();
|
| 125 |
+
const os = input.os_family;
|
| 126 |
+
if (!MOBILE.has(os)) throw new Error(`not a phone OS: ${os}`);
|
| 127 |
+
const answers = input.answers || {};
|
| 128 |
+
const apps = input.ai_apps || [];
|
| 129 |
+
const results = [];
|
| 130 |
+
const lockEvidence = input.platform_authenticator === true ? 'detected' : 'self-reported';
|
| 131 |
+
let lockSet = null;
|
| 132 |
+
|
| 133 |
+
for (const check of catalog.checks) {
|
| 134 |
+
if (!check.os.includes(os)) continue;
|
| 135 |
+
const name = (check.names && check.names[os]) || check.name;
|
| 136 |
+
const spec = (questions.checks[check.id] || {})[os];
|
| 137 |
+
let r, evidence = 'self-reported';
|
| 138 |
+
|
| 139 |
+
if (check.section === 'org') {
|
| 140 |
+
r = orgResult(check, input.org ? input.org[check.answer_key] : undefined);
|
| 141 |
+
evidence = null;
|
| 142 |
+
} else if (check.id === 'screen_lock.enabled' && input.platform_authenticator === true) {
|
| 143 |
+
r = { status: 'Compliant', actual: os === 'ios' ? 'passcode set' : 'screen lock set', expected: spec.expected,
|
| 144 |
+
detail: 'The browser reports that a device unlock method (passcode, PIN, Face ID or fingerprint) is set up.' };
|
| 145 |
+
evidence = 'detected';
|
| 146 |
+
} else if (check.id === 'gdpr.os_supported') {
|
| 147 |
+
r = osSupported(input, B);
|
| 148 |
+
evidence = input.version_source === 'user' ? 'self-reported' : 'detected';
|
| 149 |
+
} else if (check.id === 'encryption.enabled') {
|
| 150 |
+
if (os === 'android') {
|
| 151 |
+
const ok = major(input.os_version) >= 10;
|
| 152 |
+
r = { status: ok ? 'Compliant' : 'Non-Compliant', actual: ok ? `on (Android ${major(input.os_version)} encrypts by default)` : 'not guaranteed (Android older than 10)',
|
| 153 |
+
expected: 'on', detail: ok ? '' : 'Android encrypts by default only from version 10.' };
|
| 154 |
+
evidence = input.version_source === 'user' ? 'self-reported' : 'detected';
|
| 155 |
+
} else {
|
| 156 |
+
r = lockSet === null ? { status: 'Pending', actual: 'unknown', expected: 'on', detail: 'Depends on whether a passcode is set.' }
|
| 157 |
+
: { status: lockSet ? 'Compliant' : 'Non-Compliant', actual: lockSet ? 'on (passcode set)' : 'off (no passcode)', expected: 'on',
|
| 158 |
+
detail: lockSet ? 'iOS encrypts the data on the phone with hardware keys once a passcode is set.' : 'Without a passcode, iOS data protection is not active.' };
|
| 159 |
+
evidence = lockEvidence;
|
| 160 |
+
}
|
| 161 |
+
} else if (os === 'ios' && (check.id === 'vuln.pending_updates' || check.id === 'vuln.known_vulnerabilities')) {
|
| 162 |
+
evidence = input.version_source === 'user' ? 'self-reported' : 'detected';
|
| 163 |
+
const product = input.os_name === 'iPadOS' ? 'ipados' : 'ios';
|
| 164 |
+
if (!ctx.vuln || ctx.vuln.error) {
|
| 165 |
+
r = { status: 'Pending', actual: 'not checked', expected: check.id === 'vuln.pending_updates' ? 'latest iOS security release' : `<= ${B.MAX_CRITICAL_HIGH_VULNERABILITIES} critical/high`,
|
| 166 |
+
detail: `No vulnerability data available${ctx.vuln && ctx.vuln.error ? `: ${ctx.vuln.error}` : ''}.` };
|
| 167 |
+
} else if (check.id === 'vuln.pending_updates') {
|
| 168 |
+
const latest = latestIosFix(ctx.vuln.bundle, product, input.os_version);
|
| 169 |
+
const dataDate = ctx.vuln.manifest.built_at.slice(0, 10);
|
| 170 |
+
r = !latest ? { status: 'Pending', actual: `${input.os_name} ${input.os_version}`, expected: 'latest iOS security release', detail: `No security releases for ${input.os_name} ${major(input.os_version)} in the vulnerability data from ${dataDate}.` }
|
| 171 |
+
: cmp(input.os_version, latest) >= 0 ? { status: 'Compliant', actual: `${input.os_name} ${input.os_version} (up to date)`, expected: 'latest iOS security release', detail: `Newest fixed ${input.os_name} ${major(input.os_version)} release in the vulnerability data from ${dataDate}: ${latest}.${newerMajor(ctx.vuln.bundle, product, input.os_version)}` }
|
| 172 |
+
: { status: 'Non-Compliant', actual: `${input.os_name} ${input.os_version} (update to ${latest} available)`, expected: 'latest iOS security release', detail: `Install ${input.os_name} ${latest} or later in Settings › General › Software Update.` };
|
| 173 |
+
} else {
|
| 174 |
+
const inventory = { apps: [{ name: input.os_name, version: input.os_version }] };
|
| 175 |
+
const matched = matchApps(inventory, ctx.vuln.bundle, 'ios');
|
| 176 |
+
r = { expected: `<= ${B.MAX_CRITICAL_HIGH_VULNERABILITIES} critical/high`, ...knownVulnResult(matched, inventory, ctx.vuln, 'ios', parseInt(B.MAX_CRITICAL_HIGH_VULNERABILITIES, 10) || 0, now) };
|
| 177 |
+
if (input.version_source !== 'user') r.detail += ' Patch-level versions (x.y.z) are not always visible to the browser; confirm the version in Settings › General › About.';
|
| 178 |
+
}
|
| 179 |
+
} else if (check.section === 'aiact' && check.id !== 'aiact.os_ai_features') {
|
| 180 |
+
r = aiResults(check.id, apps, B);
|
| 181 |
+
} else if (spec) {
|
| 182 |
+
const a = answers[check.id];
|
| 183 |
+
if (a === undefined || a === 'not_sure') {
|
| 184 |
+
r = { ...questions.not_sure, expected: spec.expected || (spec.expected_from === 'seconds' ? `<= ${B.SCREEN_LOCK_MAX_SECONDS} seconds` : spec.expected_from === 'passcode' ? `>= ${B.MOBILE_PASSCODE_MIN_DIGITS} digits or alphanumeric` : `<= ${B.MOBILE_PATCH_MAX_AGE_DAYS} days old`) };
|
| 185 |
+
} else if (spec.input === 'month') {
|
| 186 |
+
r = patchResult(a, B, now) || { ...questions.not_sure, expected: `<= ${B.MOBILE_PATCH_MAX_AGE_DAYS} days old` };
|
| 187 |
+
} else {
|
| 188 |
+
const answer = spec.answers[a];
|
| 189 |
+
r = spec.expected_from ? computed(spec, answer, B, os)
|
| 190 |
+
: { status: answer.status, actual: answer.actual, expected: spec.expected, detail: answer.detail || '' };
|
| 191 |
+
}
|
| 192 |
+
} else {
|
| 193 |
+
throw new Error(`no rule for ${check.id} on ${os}`);
|
| 194 |
+
}
|
| 195 |
+
if (check.id === 'screen_lock.enabled') lockSet = r.status === 'Compliant' ? true : r.status === 'Non-Compliant' ? false : null;
|
| 196 |
+
const item = { id: check.id, section: check.section, name, status: r.status, actual: String(r.actual), expected: r.expected };
|
| 197 |
+
if (r.detail) item.detail = r.detail;
|
| 198 |
+
if (evidence) item.evidence = evidence;
|
| 199 |
+
results.push(item);
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
const summary = Object.fromEntries(catalog.statuses.map(s => [s, results.filter(r => r.status === s).length]));
|
| 203 |
+
const host = { hostname: input.hostname, os_family: os, os_name: input.os_name, os_version: String(input.os_version) };
|
| 204 |
+
if (input.device_model) host.device_model = input.device_model;
|
| 205 |
+
if (input.browser) host.browser = input.browser;
|
| 206 |
+
return {
|
| 207 |
+
schema_version: '1.0',
|
| 208 |
+
catalog_version: catalog.catalog_version,
|
| 209 |
+
source: { kind: 'web', collector: os, collector_version: MOBILE_VERSION },
|
| 210 |
+
audit: { mode: 'READ-ONLY', started_at: input.started_at, execution_user: 'phone self-check (browser)', is_admin: false },
|
| 211 |
+
host,
|
| 212 |
+
baseline: { ...B },
|
| 213 |
+
results,
|
| 214 |
+
summary,
|
| 215 |
+
};
|
| 216 |
+
}
|
mobile/styles.css
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
--red: #ED2939; --blue: #00A1DE; --bg: #f6f7fa; --card: #ffffff; --text: #16181d; --muted: #5b6270; --line: #e2e5eb;
|
| 3 |
+
--ok: #1a7f37; --bad: #c4242f; --pending: #9a6700; --na: #6e7781; --err: #8250df;
|
| 4 |
+
}
|
| 5 |
+
@media (prefers-color-scheme: dark) {
|
| 6 |
+
:root:not([data-theme="light"]) { --bg: #101216; --card: #1a1d23; --text: #eef0f4; --muted: #a2a9b6; --line: #2c313a; --ok: #3fb950; --bad: #ff6b6b; --pending: #d29922; --na: #8b949e; --err: #b392f0; }
|
| 7 |
+
}
|
| 8 |
+
:root[data-theme="dark"] { --bg: #101216; --card: #1a1d23; --text: #eef0f4; --muted: #a2a9b6; --line: #2c313a; --ok: #3fb950; --bad: #ff6b6b; --pending: #d29922; --na: #8b949e; --err: #b392f0; }
|
| 9 |
+
|
| 10 |
+
* { box-sizing: border-box; }
|
| 11 |
+
html, body { margin: 0; background: var(--bg); color: var(--text); }
|
| 12 |
+
body { font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; min-height: 100vh; display: flex; flex-direction: column; }
|
| 13 |
+
main { width: 100%; max-width: 560px; margin: 0 auto; padding: 8px 16px 24px; flex: 1; }
|
| 14 |
+
.hidden { display: none !important; }
|
| 15 |
+
h1 { font-size: 1.5rem; line-height: 1.25; margin: 12px 0 10px; }
|
| 16 |
+
h2 { font-size: 1rem; margin: 0; }
|
| 17 |
+
p { margin: 8px 0; }
|
| 18 |
+
a { color: var(--blue); }
|
| 19 |
+
.muted { color: var(--muted); font-size: .9rem; }
|
| 20 |
+
.lead { font-size: 1.05rem; }
|
| 21 |
+
|
| 22 |
+
.top { display: flex; justify-content: space-between; align-items: center; padding: max(12px, env(safe-area-inset-top)) 16px 10px; border-top: 4px solid var(--red); background: var(--card); border-bottom: 1px solid var(--line); }
|
| 23 |
+
.brand { display: flex; align-items: center; gap: 8px; font-size: .95rem; }
|
| 24 |
+
.mark { width: 26px; height: 26px; border-radius: 6px; background: var(--red); color: #fff; display: grid; place-items: center; font-weight: 800; }
|
| 25 |
+
.chip { font-size: .75rem; color: var(--muted); border: 1px solid var(--line); border-radius: 999px; padding: 2px 10px; max-width: 50%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
| 26 |
+
|
| 27 |
+
.card { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 14px 16px; margin: 12px 0; }
|
| 28 |
+
.device { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: .95rem; }
|
| 29 |
+
.device b { color: var(--muted); font-weight: 600; }
|
| 30 |
+
.note { border-left: 4px solid var(--blue); }
|
| 31 |
+
.facts { padding-left: 18px; margin: 14px 0; } .facts li { margin: 6px 0; }
|
| 32 |
+
.plan { columns: 2; column-gap: 18px; padding-left: 18px; margin: 6px 0 0; font-size: .85rem; } .plan li { break-inside: avoid; }
|
| 33 |
+
@media (max-width: 380px) { .plan { columns: 1; } }
|
| 34 |
+
|
| 35 |
+
button { font: inherit; border-radius: 12px; padding: 13px 16px; border: 1px solid var(--line); background: var(--card); color: var(--text); cursor: pointer; width: 100%; min-height: 48px; }
|
| 36 |
+
button.primary { background: var(--red); border-color: var(--red); color: #fff; font-weight: 700; }
|
| 37 |
+
button.primary:disabled { opacity: .5; }
|
| 38 |
+
button.secondary { background: var(--card); }
|
| 39 |
+
button.link { background: none; border: none; color: var(--blue); text-decoration: underline; }
|
| 40 |
+
.row { display: flex; gap: 10px; margin-top: 12px; } .row button { flex: 1; }
|
| 41 |
+
.actions { display: grid; gap: 10px; margin-top: 14px; }
|
| 42 |
+
|
| 43 |
+
.toggle { display: flex; gap: 12px; align-items: flex-start; cursor: pointer; }
|
| 44 |
+
.toggle input { width: 22px; height: 22px; margin-top: 2px; accent-color: var(--red); flex: none; }
|
| 45 |
+
|
| 46 |
+
.progress { height: 6px; background: var(--line); border-radius: 99px; overflow: hidden; margin: 12px 0 4px; }
|
| 47 |
+
.progress.big { height: 10px; }
|
| 48 |
+
.bar { height: 100%; width: 0; background: var(--blue); transition: width .25s; }
|
| 49 |
+
.check-name { color: var(--blue); font-size: .85rem; text-transform: uppercase; letter-spacing: .04em; margin-top: 8px; }
|
| 50 |
+
.where { background: var(--card); border: 1px dashed var(--line); border-radius: 12px; padding: 10px 12px; margin: 10px 0 14px; font-size: .92rem; }
|
| 51 |
+
.where-label { display: block; font-size: .75rem; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
|
| 52 |
+
.answers { display: grid; gap: 10px; }
|
| 53 |
+
.answers button { text-align: left; }
|
| 54 |
+
.answers button.not-sure { color: var(--muted); border-style: dashed; }
|
| 55 |
+
.answers input[type="text"], .answers input[type="month"] { font: inherit; width: 100%; padding: 12px; border-radius: 12px; border: 1px solid var(--line); background: var(--card); color: var(--text); min-height: 48px; }
|
| 56 |
+
.answers label.multi { display: flex; gap: 12px; align-items: center; background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px; min-height: 48px; }
|
| 57 |
+
.answers label.multi input { width: 22px; height: 22px; accent-color: var(--red); }
|
| 58 |
+
|
| 59 |
+
.score { font-size: 3rem; font-weight: 800; line-height: 1; }
|
| 60 |
+
.pills { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0; }
|
| 61 |
+
.pill { font-size: .8rem; font-weight: 700; border: 1px solid; border-radius: 99px; padding: 1px 9px; }
|
| 62 |
+
.s-Compliant { color: var(--ok); } .s-Non-Compliant { color: var(--bad); } .s-Pending { color: var(--pending); } .s-NotApplicable { color: var(--na); } .s-Error { color: var(--err); }
|
| 63 |
+
.gaps { padding-left: 18px; } .gaps li { margin: 6px 0; }
|
| 64 |
+
|
| 65 |
+
.foot { text-align: center; font-size: .75rem; color: var(--muted); padding: 16px 16px max(16px, env(safe-area-inset-bottom)); }
|
| 66 |
+
.center { text-align: center; }
|
mobile/test/e2e.cjs
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// End-to-end test of the phone check in a phone-sized Electron window with a phone user agent.
|
| 2 |
+
// Usage: (serve the repo root on :8765) npx electron mobile/test/e2e.cjs <ios|android> <out.json>
|
| 3 |
+
const { app, BrowserWindow } = require('electron');
|
| 4 |
+
const fs = require('node:fs');
|
| 5 |
+
const UA = {
|
| 6 |
+
ios: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Mobile/15E148 Safari/604.1',
|
| 7 |
+
android: 'Mozilla/5.0 (Linux; Android 15; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36',
|
| 8 |
+
};
|
| 9 |
+
const [os, out] = process.argv.slice(-2);
|
| 10 |
+
const PICK = `(() => {
|
| 11 |
+
const box = document.getElementById('q-answers');
|
| 12 |
+
const buttons = [...box.querySelectorAll('button')];
|
| 13 |
+
const cont = buttons.find(b => b.textContent === 'Continue');
|
| 14 |
+
const text = box.querySelector('input[type=text]'), month = box.querySelector('input[type=month]');
|
| 15 |
+
const multi = box.querySelectorAll('input[type=checkbox]');
|
| 16 |
+
if (text) { if (!text.value) text.value = '${os === 'ios' ? '26.0.1' : '15'}'; cont.click(); return 'version'; }
|
| 17 |
+
if (month) { month.value = '2026-06'; cont.click(); return 'month'; }
|
| 18 |
+
if (multi.length) { multi[0].checked = true; multi[7].checked = true; cont.click(); return 'ai'; }
|
| 19 |
+
(buttons.length > 2 ? buttons[1] : buttons[0]).click(); return 'choice';
|
| 20 |
+
})()`;
|
| 21 |
+
app.disableHardwareAcceleration();
|
| 22 |
+
app.whenReady().then(async () => {
|
| 23 |
+
const win = new BrowserWindow({ show: false, width: 390, height: 844 });
|
| 24 |
+
win.webContents.setUserAgent(UA[os]);
|
| 25 |
+
win.webContents.on('console-message', (e, level, message) => { if (level >= 2) console.log('[page]', message); });
|
| 26 |
+
const js = code => win.webContents.executeJavaScript(code);
|
| 27 |
+
const until = async (code, ms = 60000) => { const t = Date.now(); while (!(await js(code))) { if (Date.now() - t > ms) throw new Error(`timeout: ${code}`); await new Promise(r => setTimeout(r, 200)); } };
|
| 28 |
+
try {
|
| 29 |
+
await win.webContents.session.clearCache();
|
| 30 |
+
await win.loadURL('http://127.0.0.1:8765/index.html?test');
|
| 31 |
+
await until(`!document.getElementById('start').disabled`);
|
| 32 |
+
const device = await js(`document.getElementById('device-card').innerText`);
|
| 33 |
+
await js(`document.getElementById('start').click()`);
|
| 34 |
+
await js(`document.getElementById('use-ai').checked = false; document.getElementById('consent-ok').click()`);
|
| 35 |
+
let asked = [];
|
| 36 |
+
for (let i = 0; i < 60; i++) {
|
| 37 |
+
const inQuestions = await js(`!document.getElementById('s-question').classList.contains('hidden')`);
|
| 38 |
+
if (!inQuestions) break;
|
| 39 |
+
const q = await js(`document.getElementById('q-text').textContent`);
|
| 40 |
+
asked.push(q);
|
| 41 |
+
// Pick a mix of answers: the version as detected (or typed), a month, two AI apps, otherwise the 2nd option.
|
| 42 |
+
await js(PICK);
|
| 43 |
+
await new Promise(r => setTimeout(r, 50));
|
| 44 |
+
}
|
| 45 |
+
await until(`!document.getElementById('s-done').classList.contains('hidden')`, 120000);
|
| 46 |
+
const evidence = await js(`window.__devseisState.report.evidenceText`);
|
| 47 |
+
const result = await js(`document.getElementById('result').innerText`);
|
| 48 |
+
const pdfSize = await js(`window.__devseisMakePdf().then(b => b.size)`);
|
| 49 |
+
const html = await js(`window.__devseisState.report.html`);
|
| 50 |
+
fs.writeFileSync(out, evidence);
|
| 51 |
+
fs.writeFileSync(out.replace(/\.json$/, '.html'), html);
|
| 52 |
+
console.log(JSON.stringify({ os, device, questions: asked.length, pdfBytes: pdfSize, result: result.slice(0, 700) }, null, 1));
|
| 53 |
+
} catch (error) {
|
| 54 |
+
console.error('E2E FAILED:', error.message);
|
| 55 |
+
process.exitCode = 1;
|
| 56 |
+
}
|
| 57 |
+
app.quit();
|
| 58 |
+
});
|
mobile/vuln-web.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Downloads the small phone subset of the Devseis vulnerability bundle and checks its SHA-256 and
|
| 2 |
+
// Ed25519 signature before use (WebCrypto). Only data is downloaded; nothing about the phone is sent.
|
| 3 |
+
const VULNDB_URL = 'https://huggingface.co/datasets/Devseis/endpoint-auditor-vulndb/resolve/main';
|
| 4 |
+
const PUBLIC_KEY_URL = 'app/vulndb-public-key.pem';
|
| 5 |
+
|
| 6 |
+
const hex = buffer => [...new Uint8Array(buffer)].map(b => b.toString(16).padStart(2, '0')).join('');
|
| 7 |
+
const fromBase64 = text => Uint8Array.from(atob(text), c => c.charCodeAt(0));
|
| 8 |
+
|
| 9 |
+
async function verifySignature(sha, signature) {
|
| 10 |
+
const pem = await (await fetch(PUBLIC_KEY_URL)).text();
|
| 11 |
+
const der = fromBase64(pem.replace(/-----[^-]+-----/g, '').replace(/\s+/g, ''));
|
| 12 |
+
try {
|
| 13 |
+
const key = await crypto.subtle.importKey('spki', der, { name: 'Ed25519' }, false, ['verify']);
|
| 14 |
+
return await crypto.subtle.verify({ name: 'Ed25519' }, key, fromBase64(signature), new TextEncoder().encode(sha));
|
| 15 |
+
} catch (error) {
|
| 16 |
+
if (error && error.name === 'NotSupportedError') return 'unsupported'; // older browsers: SHA-256 over HTTPS only
|
| 17 |
+
throw error;
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export async function loadPhoneVulnData() {
|
| 22 |
+
try {
|
| 23 |
+
const manifest = await (await fetch(`${VULNDB_URL}/manifest.json`, { cache: 'no-cache' })).json();
|
| 24 |
+
if (!manifest.mobile) throw new Error('the vulnerability data has no phone subset');
|
| 25 |
+
const data = await (await fetch(`${VULNDB_URL}/${manifest.mobile.file}`)).arrayBuffer();
|
| 26 |
+
const sha = hex(await crypto.subtle.digest('SHA-256', data));
|
| 27 |
+
if (sha !== manifest.mobile.sha256) throw new Error('vulnerability data checksum does not match');
|
| 28 |
+
const signed = await verifySignature(sha, manifest.mobile.signature);
|
| 29 |
+
if (signed === false) throw new Error('vulnerability data signature is not valid');
|
| 30 |
+
const text = await new Response(new Blob([data]).stream().pipeThrough(new DecompressionStream('gzip'))).text();
|
| 31 |
+
return { manifest, bundle: JSON.parse(text), signatureChecked: signed === true };
|
| 32 |
+
} catch (error) {
|
| 33 |
+
return { error: error.message };
|
| 34 |
+
}
|
| 35 |
+
}
|
schema/evidence.schema.json
CHANGED
|
@@ -14,8 +14,8 @@
|
|
| 14 |
"required": ["kind", "collector", "collector_version"],
|
| 15 |
"additionalProperties": false,
|
| 16 |
"properties": {
|
| 17 |
-
"kind": { "enum": ["collector", "synthetic"] },
|
| 18 |
-
"collector": { "enum": ["windows", "linux", "macos"] },
|
| 19 |
"collector_version": { "type": "string" },
|
| 20 |
"synthetic_seed": { "type": "integer" },
|
| 21 |
"synthetic_profile": { "type": "string" }
|
|
@@ -38,7 +38,9 @@
|
|
| 38 |
"additionalProperties": false,
|
| 39 |
"properties": {
|
| 40 |
"hostname": { "type": "string" },
|
| 41 |
-
"os_family": { "enum": ["windows", "linux", "macos"] },
|
|
|
|
|
|
|
| 42 |
"os_name": { "type": "string" },
|
| 43 |
"os_version": { "type": "string" },
|
| 44 |
"build": { "type": "string" },
|
|
@@ -65,7 +67,8 @@
|
|
| 65 |
"status": { "enum": ["Compliant", "Non-Compliant", "NotApplicable", "Pending", "Error"] },
|
| 66 |
"actual": { "type": "string", "description": "What was found on the device" },
|
| 67 |
"expected": { "type": "string", "description": "What the baseline requires" },
|
| 68 |
-
"detail": { "type": "string", "description": "How the value was read, or why the status was given" }
|
|
|
|
| 69 |
}
|
| 70 |
}
|
| 71 |
},
|
|
|
|
| 14 |
"required": ["kind", "collector", "collector_version"],
|
| 15 |
"additionalProperties": false,
|
| 16 |
"properties": {
|
| 17 |
+
"kind": { "enum": ["collector", "synthetic", "web"] },
|
| 18 |
+
"collector": { "enum": ["windows", "linux", "macos", "ios", "android"] },
|
| 19 |
"collector_version": { "type": "string" },
|
| 20 |
"synthetic_seed": { "type": "integer" },
|
| 21 |
"synthetic_profile": { "type": "string" }
|
|
|
|
| 38 |
"additionalProperties": false,
|
| 39 |
"properties": {
|
| 40 |
"hostname": { "type": "string" },
|
| 41 |
+
"os_family": { "enum": ["windows", "linux", "macos", "ios", "android"] },
|
| 42 |
+
"device_model": { "type": "string" },
|
| 43 |
+
"browser": { "type": "string" },
|
| 44 |
"os_name": { "type": "string" },
|
| 45 |
"os_version": { "type": "string" },
|
| 46 |
"build": { "type": "string" },
|
|
|
|
| 67 |
"status": { "enum": ["Compliant", "Non-Compliant", "NotApplicable", "Pending", "Error"] },
|
| 68 |
"actual": { "type": "string", "description": "What was found on the device" },
|
| 69 |
"expected": { "type": "string", "description": "What the baseline requires" },
|
| 70 |
+
"detail": { "type": "string", "description": "How the value was read, or why the status was given" },
|
| 71 |
+
"evidence": { "enum": ["detected", "self-reported"], "description": "Phones: detected by the browser, or answered by the user. Absent = read by a collector." }
|
| 72 |
}
|
| 73 |
}
|
| 74 |
},
|
synthetic/generate_evidence.py
CHANGED
|
@@ -12,14 +12,16 @@ Usage:
|
|
| 12 |
Standard library only.
|
| 13 |
"""
|
| 14 |
import argparse
|
|
|
|
| 15 |
import json
|
|
|
|
| 16 |
import os
|
| 17 |
import random
|
| 18 |
import string
|
| 19 |
from datetime import datetime, timedelta, timezone
|
| 20 |
|
| 21 |
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 22 |
-
GENERATOR_VERSION = "0.
|
| 23 |
|
| 24 |
# Report order per OS, identical to the collectors.
|
| 25 |
PASSWORD_LOCKOUT = ["password.min_length", "password.history", "password.expiration", "password.min_age",
|
|
@@ -83,6 +85,66 @@ AI_TOOLS = {
|
|
| 83 |
REMOTE_SERVICES = {"windows": ["rdp", "winrm", "ssh", "smb", "teamviewer", "anydesk", "chromeremotedesktop"],
|
| 84 |
"linux": ["ssh", "rdp", "vnc", "smb", "teamviewer", "anydesk", "chromeremotedesktop"],
|
| 85 |
"macos": ["ssh", "screensharing", "filesharing", "remotemanagement", "chromeremotedesktop", "teamviewer", "anydesk"]}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
EMPLOYEE_ACCOUNTS = ["jdoe", "asmith", "mkhan", "lmuller", "pmartin", "sali", "tnguyen", "rschmit", "akaya", "ewagner"]
|
| 87 |
|
| 88 |
|
|
@@ -549,47 +611,116 @@ class Device:
|
|
| 549 |
def c_vuln_pending_updates(self):
|
| 550 |
limit = self.n("MAX_PENDING_SECURITY_UPDATES")
|
| 551 |
expected = f"<= {limit} pending"
|
| 552 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 553 |
return ("Compliant", "0 pending", expected, "")
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
return ("Non-Compliant", f"{n} pending", expected, "Including: " + ", ".join(listed) + ".")
|
| 563 |
|
| 564 |
def c_vuln_outdated_apps(self):
|
| 565 |
limit = self.n("MAX_OUTDATED_APPS")
|
| 566 |
expected = f"<= {limit} outdated"
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 578 |
|
| 579 |
def c_vuln_known_vulnerabilities(self):
|
| 580 |
-
|
| 581 |
-
expected = f"<= {
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 593 |
|
| 594 |
# --- GDPR ----------------------------------------------------------------------------
|
| 595 |
def c_gdpr_auto_updates(self):
|
|
@@ -763,6 +894,98 @@ def org_result(rng, profile, answer_key):
|
|
| 763 |
return ("Pending", "not answered", "yes", f"Answer {answer_key} in organisational.conf.")
|
| 764 |
|
| 765 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 766 |
def random_hostname(rng, os_family):
|
| 767 |
if os_family == "windows":
|
| 768 |
return rng.choice(["DESKTOP-", "LAPTOP-"]) + "".join(rng.choices(string.ascii_uppercase + string.digits, k=7))
|
|
@@ -818,6 +1041,8 @@ def make_record(rng, seed, os_family, profile, baseline, catalog, checks):
|
|
| 818 |
"status": status, "actual": actual, "expected": expected}
|
| 819 |
if detail:
|
| 820 |
item["detail"] = detail
|
|
|
|
|
|
|
| 821 |
results.append(item)
|
| 822 |
|
| 823 |
summary = {s: sum(1 for r in results if r["status"] == s) for s in catalog["statuses"]}
|
|
@@ -839,7 +1064,7 @@ def main():
|
|
| 839 |
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 840 |
parser.add_argument("--count", type=int, default=100, help="number of audit runs to generate")
|
| 841 |
parser.add_argument("--seed", type=int, default=1, help="random seed (same seed = same data)")
|
| 842 |
-
parser.add_argument("--os", default="windows,linux,macos", help="comma-separated OS families")
|
| 843 |
parser.add_argument("--baseline", default=os.path.join(ROOT, "baseline.conf"))
|
| 844 |
parser.add_argument("--out", default=os.path.join(ROOT, "synthetic", "out", "evidence.jsonl"), help="JSONL output file")
|
| 845 |
parser.add_argument("--files", help="write one pretty JSON file per run into this folder instead of JSONL")
|
|
@@ -849,12 +1074,28 @@ def main():
|
|
| 849 |
baseline = read_conf(args.baseline)
|
| 850 |
families = [f.strip() for f in args.os.split(",") if f.strip()]
|
| 851 |
rng = random.Random(args.seed)
|
| 852 |
-
records = []
|
|
|
|
| 853 |
for index in range(args.count):
|
| 854 |
record_seed = args.seed * 1_000_003 + index
|
| 855 |
local = random.Random(record_seed)
|
| 856 |
-
|
| 857 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 858 |
|
| 859 |
if args.files:
|
| 860 |
os.makedirs(args.files, exist_ok=True)
|
|
|
|
| 12 |
Standard library only.
|
| 13 |
"""
|
| 14 |
import argparse
|
| 15 |
+
import gzip
|
| 16 |
import json
|
| 17 |
+
import subprocess
|
| 18 |
import os
|
| 19 |
import random
|
| 20 |
import string
|
| 21 |
from datetime import datetime, timedelta, timezone
|
| 22 |
|
| 23 |
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 24 |
+
GENERATOR_VERSION = "0.3.0"
|
| 25 |
|
| 26 |
# Report order per OS, identical to the collectors.
|
| 27 |
PASSWORD_LOCKOUT = ["password.min_length", "password.history", "password.expiration", "password.min_age",
|
|
|
|
| 85 |
REMOTE_SERVICES = {"windows": ["rdp", "winrm", "ssh", "smb", "teamviewer", "anydesk", "chromeremotedesktop"],
|
| 86 |
"linux": ["ssh", "rdp", "vnc", "smb", "teamviewer", "anydesk", "chromeremotedesktop"],
|
| 87 |
"macos": ["ssh", "screensharing", "filesharing", "remotemanagement", "chromeremotedesktop", "teamviewer", "anydesk"]}
|
| 88 |
+
MAC_CODENAMES = {"26": "Tahoe", "15": "Sequoia", "14": "Sonoma", "13": "Ventura"}
|
| 89 |
+
# Installed-program names as the collectors report them, per vulnerability-bundle product (vulndb/products.json).
|
| 90 |
+
APP_NAMES = {
|
| 91 |
+
"windows": {"chrome": "Google Chrome", "edge": "Microsoft Edge", "firefox": "Mozilla Firefox (x64 en-US)", "acrobat_reader": "Adobe Acrobat Reader (64-bit)",
|
| 92 |
+
"7zip": "7-Zip {v} (x64)", "winrar": "WinRAR {v} (64-bit)", "notepadpp": "Notepad++ (64-bit x64)", "vlc": "VLC media player",
|
| 93 |
+
"zoom": "Zoom Workplace", "teams": "Microsoft Teams", "vscode": "Microsoft Visual Studio Code (User)", "putty": "PuTTY release {v} (64-bit)",
|
| 94 |
+
"winscp": "WinSCP {v}", "filezilla": "FileZilla {v}", "wireshark": "Wireshark {v} x64", "git": "Git", "python": "Python {v} (64-bit)",
|
| 95 |
+
"nodejs": "Node.js", "keepass": "KeePass Password Safe {v}", "teamviewer": "TeamViewer", "anydesk": "AnyDesk", "docker_desktop": "Docker Desktop",
|
| 96 |
+
"libreoffice": "LibreOffice {v}", "gimp": "GIMP {v}", "foxit_reader": "Foxit PDF Reader", "telegram": "Telegram Desktop"},
|
| 97 |
+
"macos": {"chrome": "Google Chrome", "edge": "Microsoft Edge", "firefox": "Firefox", "thunderbird": "Thunderbird", "acrobat_reader": "Adobe Acrobat Reader",
|
| 98 |
+
"vlc": "VLC", "zoom": "zoom.us", "teams": "Microsoft Teams", "vscode": "Visual Studio Code", "filezilla": "FileZilla", "wireshark": "Wireshark",
|
| 99 |
+
"teamviewer": "TeamViewer", "anydesk": "AnyDesk", "docker_desktop": "Docker", "openvpn_connect": "OpenVPN Connect", "libreoffice": "LibreOffice",
|
| 100 |
+
"gimp": "GIMP", "telegram": "Telegram", "safari": "Safari", "iterm2": "iTerm"},
|
| 101 |
+
}
|
| 102 |
+
COMMON_APPS = {"windows": ["chrome", "edge", "zoom", "teams", "acrobat_reader", "7zip", "vscode", "firefox", "notepadpp", "vlc"],
|
| 103 |
+
"macos": ["safari", "chrome", "zoom", "teams", "vscode", "acrobat_reader", "firefox", "vlc", "docker_desktop", "iterm2"]}
|
| 104 |
+
FILLER_APPS = {"windows": ["Microsoft 365 Apps for business - en-us", "Slack", "Notion", "1Password", "Spotify", "Dell SupportAssist",
|
| 105 |
+
"Microsoft Visual C++ 2015-2022 Redistributable (x64)", "NVIDIA Graphics Driver", "WhatsApp", "Logitech Options+", "Intel(R) Graphics Driver"],
|
| 106 |
+
"macos": ["Slack", "Notion", "1Password", "Microsoft Word", "Microsoft Excel", "Spotify", "WhatsApp", "Figma", "Postman", "Raycast", "Keynote", "Pages"]}
|
| 107 |
+
BREW_FORMULAE = ["openssl@3", "python@3.13", "node", "git", "curl", "wget", "ca-certificates", "sqlite", "libpng", "icu4c@77", "jq", "gh", "ffmpeg", "pkgconf", "readline", "xz"]
|
| 108 |
+
WINGET_IDS = {"Google Chrome": "Google.Chrome", "Mozilla Firefox (x64 en-US)": "Mozilla.Firefox", "Zoom Workplace": "Zoom.Zoom", "7-Zip 24.08 (x64)": "7zip.7zip",
|
| 109 |
+
"Notepad++ (64-bit x64)": "Notepad++.Notepad++", "VLC media player": "VideoLAN.VLC", "Microsoft Visual Studio Code (User)": "Microsoft.VisualStudioCode",
|
| 110 |
+
"Adobe Acrobat Reader (64-bit)": "Adobe.Acrobat.Reader.64-bit", "Git": "Git.Git", "Slack": "SlackTechnologies.Slack"}
|
| 111 |
+
_BUNDLE = None
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def vuln_bundle():
|
| 115 |
+
"""Real offline vulnerability bundle (vulndb/out), used to draw realistic installed versions."""
|
| 116 |
+
global _BUNDLE
|
| 117 |
+
if _BUNDLE is None:
|
| 118 |
+
out = os.path.join(ROOT, "vulndb", "out")
|
| 119 |
+
manifest = json.load(open(os.path.join(out, "manifest.json")))
|
| 120 |
+
with gzip.open(os.path.join(out, manifest["file"])) as handle:
|
| 121 |
+
_BUNDLE = json.load(handle)
|
| 122 |
+
_BUNDLE["_manifest"] = manifest
|
| 123 |
+
return _BUNDLE
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def vkey(v):
|
| 127 |
+
return [int(x) if x.isdigit() else 0 for x in v.replace("-", ".").split(".")]
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def fixed_versions(product):
|
| 131 |
+
"""Real release numbers from the NVD ranges (versions that fixed something), oldest first."""
|
| 132 |
+
return sorted({row[5] for row in product["cves"] if row[5]}, key=vkey)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def current_version(product):
|
| 136 |
+
"""A version newer than every affected one in NVD: what a fully updated device runs."""
|
| 137 |
+
bounds = sorted({v for row in product["cves"] for v in (row[4], row[5], row[6]) if v}, key=vkey)
|
| 138 |
+
top = bounds[-1]
|
| 139 |
+
if any(top in (row[4], row[6]) for row in product["cves"]): # still affected at that version: one patch newer
|
| 140 |
+
parts = top.split(".")
|
| 141 |
+
parts[-1] = str(int(parts[-1]) + 1) if parts[-1].isdigit() else parts[-1] + ".1"
|
| 142 |
+
top = ".".join(parts)
|
| 143 |
+
return top
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
VULN_JOBS = [] # (result item, job) filled by Device.c_vuln_known_vulnerabilities, resolved by the real matcher in main()
|
| 147 |
+
|
| 148 |
EMPLOYEE_ACCOUNTS = ["jdoe", "asmith", "mkhan", "lmuller", "pmartin", "sali", "tnguyen", "rschmit", "akaya", "ewagner"]
|
| 149 |
|
| 150 |
|
|
|
|
| 611 |
def c_vuln_pending_updates(self):
|
| 612 |
limit = self.n("MAX_PENDING_SECURITY_UPDATES")
|
| 613 |
expected = f"<= {limit} pending"
|
| 614 |
+
good = self.good(-0.05)
|
| 615 |
+
if self.os == "macos":
|
| 616 |
+
if self.rng.random() < 0.04:
|
| 617 |
+
return ("Error", "not read", expected, "Apple's update service could not be reached.")
|
| 618 |
+
if good:
|
| 619 |
+
return ("Compliant", "0 pending", expected, "")
|
| 620 |
+
major = self.os_version.split(".")[0]
|
| 621 |
+
parts = self.os_version.split(".") + ["0", "0"]
|
| 622 |
+
newer = f"{major}.{int(parts[1]) + self.pick([0, 1, 2])}.{int(parts[2]) + 1}"
|
| 623 |
+
labels = [f"macOS {MAC_CODENAMES.get(major, '')} {newer}-{self.pick(['25G', '24H', '23J', '22H'])}{self.rng.randint(100, 899)}".replace(" ", " ")]
|
| 624 |
+
if self.rng.random() < 0.3:
|
| 625 |
+
labels.append(f"Command Line Tools for Xcode-{self.pick(['16.4', '26.0', '26.1'])}")
|
| 626 |
+
if self.rng.random() < 0.2:
|
| 627 |
+
labels.append(f"Safari{self.pick(['26.0.1', '26.1', '18.6'])}{MAC_CODENAMES.get(major, '')}-{self.pick(['26.0.1', '26.1', '18.6'])}")
|
| 628 |
+
return ("Non-Compliant", f"{len(labels)} pending", expected, f"Including: {', '.join(labels)}.")
|
| 629 |
+
if self.os == "linux":
|
| 630 |
+
note = (f" Package lists are {self.pick([0, 0, 1, 2, 3, 7, 14, 30])} days old." if self.distro in ("ubuntu", "debian") else " Uses the cached repository data.")
|
| 631 |
+
if good:
|
| 632 |
+
return ("Compliant", "0 pending", expected, note.strip())
|
| 633 |
+
names = (["libssl3t64", "openssl", "libc6", "libc-bin", "linux-image-generic", "openssh-server", "sudo", "curl", "libcurl4t64", "libxml2", "python3.12", "systemd", "libsystemd0", "tzdata"]
|
| 634 |
+
if self.distro != "rhel" else ["openssl-libs", "krb5-libs", "glibc", "glibc-common", "kernel", "curl-minimal", "libxml2", "sudo", "systemd-libs", "python3-libs", "vim-minimal"])
|
| 635 |
+
n = self.rng.randint(1, 40)
|
| 636 |
+
listed = self.rng.sample(names, min(n, 3))
|
| 637 |
+
return ("Non-Compliant", f"{n} pending", expected, f"Including: {', '.join(listed)}.{note}")
|
| 638 |
+
if self.rng.random() < 0.04:
|
| 639 |
+
return ("Error", "not read", expected, "No answer within 300 seconds.")
|
| 640 |
+
if good:
|
| 641 |
return ("Compliant", "0 pending", expected, "")
|
| 642 |
+
version = {"26200": "25H2", "26100": "24H2", "22631": "23H2", "22621": "22H2", "19045": "22H2"}.get(self.build, "24H2")
|
| 643 |
+
product = "Windows 10" if self.build == "19045" else "Windows 11"
|
| 644 |
+
titles = [f"2026-{self.pick(['09', '10'])} Cumulative Update for {product} Version {version} for x64-based Systems (KB50{self.rng.randint(41000, 49999)})"]
|
| 645 |
+
for extra in ("2026-{m} Cumulative Update for .NET Framework 3.5 and 4.8.1 for {p} Version {v} for x64 (KB50{k})",
|
| 646 |
+
"Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.4{k3}.0)"):
|
| 647 |
+
if self.rng.random() < 0.35:
|
| 648 |
+
titles.append(extra.format(m=self.pick(["09", "10"]), p=product, v=version, k=self.rng.randint(41000, 49999), k3=self.rng.randint(10, 99)))
|
| 649 |
+
return ("Non-Compliant", f"{len(titles)} pending", expected, f"Including: {', '.join(titles[:3])}.")
|
|
|
|
| 650 |
|
| 651 |
def c_vuln_outdated_apps(self):
|
| 652 |
limit = self.n("MAX_OUTDATED_APPS")
|
| 653 |
expected = f"<= {limit} outdated"
|
| 654 |
+
good = self.good(-0.15)
|
| 655 |
+
if self.os == "macos":
|
| 656 |
+
if self.rng.random() < 0.45:
|
| 657 |
+
return ("NotApplicable", "not checked", expected, "Homebrew is not installed, so there is no local source of current application versions.")
|
| 658 |
+
if good:
|
| 659 |
+
return ("Compliant", "0 outdated", expected, "Homebrew packages checked; App Store apps are not covered.")
|
| 660 |
+
n = self.rng.randint(1, 70)
|
| 661 |
+
rows = []
|
| 662 |
+
for formula in self.rng.sample(BREW_FORMULAE, min(n, 5)):
|
| 663 |
+
a, b = self.rng.randint(1, 30), self.rng.randint(0, 20)
|
| 664 |
+
rows.append(f"{formula} ({a}.{b}.{self.rng.randint(0, 9)}) < {a}.{b + self.pick([0, 1, 2])}.{self.rng.randint(10, 19)}")
|
| 665 |
+
return ("Non-Compliant", f"{n} outdated", expected, "; ".join(rows) + ".")
|
| 666 |
+
if self.os == "linux":
|
| 667 |
+
packages = 0 if good else self.rng.randint(0, 60)
|
| 668 |
+
parts, total = [f"packages: {packages}"], packages
|
| 669 |
+
if not good and self.distro == "ubuntu" and self.rng.random() < 0.5:
|
| 670 |
+
snaps = self.rng.sample(["firefox", "snap-store", "thunderbird", "code", "chromium", "slack", "zoom-client"], self.pick([1, 2, 3]))
|
| 671 |
+
parts.append("snaps: " + ", ".join(snaps)); total += len(snaps)
|
| 672 |
+
if not good and self.rng.random() < 0.25:
|
| 673 |
+
flats = self.rng.sample(["org.mozilla.firefox", "com.slack.Slack", "us.zoom.Zoom", "org.libreoffice.LibreOffice", "org.gimp.GIMP"], self.pick([1, 2]))
|
| 674 |
+
parts.append("flatpaks: " + ", ".join(flats)); total += len(flats)
|
| 675 |
+
return self.result(total <= limit, f"{total} outdated", expected, "; ".join(parts))
|
| 676 |
+
if self.rng.random() < 0.15:
|
| 677 |
+
return ("NotApplicable", "not checked", expected, "winget (App Installer) is not available for this account, so there is no local source of current application versions.")
|
| 678 |
+
if good:
|
| 679 |
+
return ("Compliant", "0 outdated", expected, self.pick(["winget found no upgrades for packages it can match.", ""]))
|
| 680 |
+
rows = []
|
| 681 |
+
for name in self.rng.sample(list(WINGET_IDS), self.pick([1, 2, 3, 4, 6])):
|
| 682 |
+
a = self.rng.randint(1, 130)
|
| 683 |
+
rows.append(f"{name} {WINGET_IDS[name]} {a}.{self.rng.randint(0, 9)}.{self.rng.randint(0, 999)} {a + self.pick([0, 1])}.{self.rng.randint(0, 9)}.{self.rng.randint(0, 999)} winget")
|
| 684 |
+
return ("Non-Compliant", f"{len(rows)} outdated", expected, "; ".join(rows[:5]) + ".")
|
| 685 |
|
| 686 |
def c_vuln_known_vulnerabilities(self):
|
| 687 |
+
"""Builds a realistic software inventory; the real matcher fills in the result (see VULN_JOBS)."""
|
| 688 |
+
expected = f"<= {self.n('MAX_CRITICAL_HIGH_VULNERABILITIES')} critical/high"
|
| 689 |
+
bundle = vuln_bundle()
|
| 690 |
+
stale = {"neglected": 0.9, "typical": 0.5, "hardened": 0.04}[self.profile]
|
| 691 |
+
if self.os == "linux":
|
| 692 |
+
key = {"ubuntu": f"ubuntu:{self.os_version}", "debian": f"debian:{self.os_version}", "rhel": f"rhel:{self.os_version.split('.')[0]}"}[self.distro]
|
| 693 |
+
kind = "rpm" if self.distro == "rhel" else "deb"
|
| 694 |
+
packages = []
|
| 695 |
+
advisories = bundle["releases"].get(key, {})
|
| 696 |
+
# Cloud/OEM kernel flavours (linux-aws, linux-ibm, ...) are not on ordinary endpoints.
|
| 697 |
+
names = sorted(n for n in advisories if not (n.startswith("linux-") and not n.startswith(("linux-firmware", "linux-signed"))))
|
| 698 |
+
per_package = {"neglected": 0.12, "typical": 0.03, "hardened": 0.002}[self.profile]
|
| 699 |
+
for name in self.rng.sample(names, min(len(names), self.rng.randint(300, 900))):
|
| 700 |
+
entries = advisories[name]
|
| 701 |
+
if self.rng.random() < per_package:
|
| 702 |
+
version = self.pick(entries)[2] # an older build: the matcher decides what still applies
|
| 703 |
+
else:
|
| 704 |
+
version = "99:99" if kind == "deb" else "99:99-1" # newer than every fix (never shown in findings)
|
| 705 |
+
packages.append({"name": name, "version": version, "source": "", "source_version": ""})
|
| 706 |
+
inventory = {"format": 1, "os_family": "linux", "distro_id": self.distro, "version_id": self.os_version, "pretty_name": self.os_name, "kind": kind, "packages": packages}
|
| 707 |
+
else:
|
| 708 |
+
apps = []
|
| 709 |
+
products = {p["id"]: p for p in bundle["apps"]}
|
| 710 |
+
names = APP_NAMES[self.os]
|
| 711 |
+
chosen = [pid for pid in names if pid in products and (pid in COMMON_APPS[self.os] or self.rng.random() < 0.15)]
|
| 712 |
+
for pid in chosen:
|
| 713 |
+
versions = fixed_versions(products[pid])
|
| 714 |
+
if not versions:
|
| 715 |
+
continue
|
| 716 |
+
version = self.pick(versions[-12:-1]) if (self.rng.random() < stale * 0.5 and len(versions) > 2) else current_version(products[pid])
|
| 717 |
+
apps.append({"name": names[pid].format(v=version), "version": version})
|
| 718 |
+
for filler in self.rng.sample(FILLER_APPS[self.os], self.rng.randint(3, len(FILLER_APPS[self.os]))):
|
| 719 |
+
apps.append({"name": filler, "version": f"{self.rng.randint(1, 30)}.{self.rng.randint(0, 9)}.{self.rng.randint(0, 99)}"})
|
| 720 |
+
inventory = {"format": 1, "os_family": self.os, "apps": apps}
|
| 721 |
+
self.vuln_job = {"os_family": self.os, "inventory": inventory, "max": self.n("MAX_CRITICAL_HIGH_VULNERABILITIES"),
|
| 722 |
+
"now": self.started.isoformat(timespec="seconds")}
|
| 723 |
+
return ("Pending", "matching", expected, "")
|
| 724 |
|
| 725 |
# --- GDPR ----------------------------------------------------------------------------
|
| 726 |
def c_gdpr_auto_updates(self):
|
|
|
|
| 894 |
return ("Pending", "not answered", "yes", f"Answer {answer_key} in organisational.conf.")
|
| 895 |
|
| 896 |
|
| 897 |
+
# --- Phones: answer sets for the mobile web check (mobile/mobile-core.js builds the evidence) ---------------
|
| 898 |
+
ANDROID_MODELS = ["Pixel 9", "Pixel 8", "Pixel 7a", "Galaxy S25", "Galaxy S24", "Galaxy A55", "Galaxy A35", "Galaxy Tab S9", "Xiaomi 14", "OnePlus 12", "Motorola edge 50"]
|
| 899 |
+
MOBILE_AI_WEIGHTS = {"ChatGPT": 10, "Gemini": 7, "Microsoft Copilot": 5, "Claude": 3, "Perplexity": 3, "Meta AI": 3, "Grok": 2, "DeepSeek": 2,
|
| 900 |
+
"Character.AI": 1, "Replika": 1, "Otter": 2, "Grammarly": 2}
|
| 901 |
+
|
| 902 |
+
|
| 903 |
+
def mobile_questions():
|
| 904 |
+
with open(os.path.join(ROOT, "catalog", "mobile_questions.json"), encoding="utf-8") as handle:
|
| 905 |
+
return json.load(handle)
|
| 906 |
+
|
| 907 |
+
|
| 908 |
+
def answer_ok(spec, answer, baseline):
|
| 909 |
+
if spec.get("expected_from") == "seconds":
|
| 910 |
+
return answer["seconds"] is not None and 0 < answer["seconds"] <= int(baseline["SCREEN_LOCK_MAX_SECONDS"])
|
| 911 |
+
if spec.get("expected_from") == "passcode":
|
| 912 |
+
return bool(answer.get("alphanumeric")) or (answer.get("digits") or 0) >= int(baseline["MOBILE_PASSCODE_MIN_DIGITS"])
|
| 913 |
+
return answer.get("status") in ("Compliant", "NotApplicable")
|
| 914 |
+
|
| 915 |
+
|
| 916 |
+
def make_mobile_input(rng, seed, os_family, profile, baseline, questions):
|
| 917 |
+
p_good = PROFILES[profile]
|
| 918 |
+
good = lambda bias=0.0: rng.random() < min(max(p_good + bias, 0.02), 0.98)
|
| 919 |
+
offset = timezone(timedelta(hours=rng.choice([1, 2, 5])))
|
| 920 |
+
started = datetime(2026, 10, 1, tzinfo=offset) + timedelta(minutes=rng.randint(0, 60 * 24 * 6))
|
| 921 |
+
team = rng.choice(["fin", "hr", "dev", "ops", "sales", "legal", "mkt"])
|
| 922 |
+
bundle = vuln_bundle()
|
| 923 |
+
if os_family == "ios":
|
| 924 |
+
ipad = rng.random() < 0.15
|
| 925 |
+
os_name = "iPadOS" if ipad else "iOS"
|
| 926 |
+
product = next(a for a in bundle["apps"] if a["id"] == ("ipados" if ipad else "ios"))
|
| 927 |
+
versions = fixed_versions(product)
|
| 928 |
+
lines = {m: [v for v in versions if v.split(".")[0] == m] for m in ("27", "26", "18", "17")}
|
| 929 |
+
if good(0.05): # up to date: newest release, or the newest of the previous line (older device)
|
| 930 |
+
version = current_version(product) if rng.random() < 0.7 else rng.choice([lines["26"][-1], lines["18"][-1]])
|
| 931 |
+
else:
|
| 932 |
+
version = rng.choice(lines["26"][-8:-1] + lines["18"][-6:-1] + lines["17"][-3:])
|
| 933 |
+
version_source = "browser" if rng.random() < 0.6 else "user"
|
| 934 |
+
shown = ".".join(version.split(".")[:2]) if version_source == "browser" else version
|
| 935 |
+
browser = f"Safari {'.'.join(version.split('.')[:2])}" if rng.random() < 0.88 else f"Chrome {rng.randint(138, 142)} (iOS)"
|
| 936 |
+
model = "iPad" if ipad else "iPhone"
|
| 937 |
+
hostname = f"{team.upper()}-{model}-{rng.randint(1, 60):02d}"
|
| 938 |
+
else:
|
| 939 |
+
os_name = "Android"
|
| 940 |
+
version = str(rng.choice([16, 16, 15, 15, 14]) if good() else rng.choice([15, 14, 13, 12, 11, 10]))
|
| 941 |
+
version_source, shown = ("browser" if rng.random() < 0.85 else "user"), version
|
| 942 |
+
browser = rng.choice([f"Chrome {rng.randint(138, 142)}", f"Chrome {rng.randint(138, 142)}", "Samsung Internet 28.0", "Firefox 143"])
|
| 943 |
+
model = rng.choice(ANDROID_MODELS)
|
| 944 |
+
hostname = f"{team}-{model.split()[0].lower()}-{rng.randint(1, 60):02d}"
|
| 945 |
+
|
| 946 |
+
lock_set = good(0.25)
|
| 947 |
+
platform_authenticator = (True if rng.random() < 0.8 else rng.choice([None, False])) if lock_set else False
|
| 948 |
+
answers = {}
|
| 949 |
+
for check_id, per_os in questions["checks"].items():
|
| 950 |
+
spec = per_os.get(os_family)
|
| 951 |
+
if not spec:
|
| 952 |
+
continue
|
| 953 |
+
if check_id == "screen_lock.enabled":
|
| 954 |
+
if platform_authenticator is True:
|
| 955 |
+
continue
|
| 956 |
+
answers[check_id] = 0 if lock_set else 1
|
| 957 |
+
continue
|
| 958 |
+
if rng.random() < 0.04:
|
| 959 |
+
answers[check_id] = "not_sure"
|
| 960 |
+
continue
|
| 961 |
+
if spec.get("input") == "month":
|
| 962 |
+
months_back = rng.randint(0, 2) if good() else rng.randint(3, 20)
|
| 963 |
+
month = (started.year * 12 + started.month - 1) - months_back
|
| 964 |
+
answers[check_id] = f"{month // 12:04d}-{month % 12 + 1:02d}"
|
| 965 |
+
continue
|
| 966 |
+
ok_list = [i for i, a in enumerate(spec["answers"]) if answer_ok(spec, a, baseline)]
|
| 967 |
+
bad_list = [i for i, a in enumerate(spec["answers"]) if not answer_ok(spec, a, baseline)]
|
| 968 |
+
bias = 0.25 if check_id in ("mobile.jailbreak_root", "mobile.usb_debugging", "gdpr.time_sync") else 0.0
|
| 969 |
+
pool = ok_list if (good(bias) or not bad_list) else bad_list
|
| 970 |
+
answers[check_id] = rng.choice(pool)
|
| 971 |
+
ai_apps = [] if rng.random() < 0.4 else sorted(set(rng.choices(list(MOBILE_AI_WEIGHTS), weights=list(MOBILE_AI_WEIGHTS.values()), k=rng.choice([1, 1, 2, 3]))))
|
| 972 |
+
org = None
|
| 973 |
+
if rng.random() < 0.4:
|
| 974 |
+
choices, weights = zip(*ORG_ANSWERS[profile])
|
| 975 |
+
org = {key: rng.choices(choices, weights=weights)[0] for key in ("ORG_ROPA", "ORG_DPIA", "ORG_BREACH_PROCEDURE", "ORG_PROCESSOR_AGREEMENTS",
|
| 976 |
+
"ORG_AI_REGISTER", "ORG_AI_LITERACY", "ORG_HUMAN_OVERSIGHT", "ORG_AI_TRANSPARENCY")}
|
| 977 |
+
return {"os_family": os_family, "os_name": os_name, "os_version": shown, "version_source": version_source, "browser": browser,
|
| 978 |
+
"device_model": model, "hostname": hostname, "started_at": started.isoformat(timespec="seconds"), "now": started.isoformat(timespec="seconds"),
|
| 979 |
+
"platform_authenticator": platform_authenticator, "answers": answers, "ai_apps": ai_apps, "org": org,
|
| 980 |
+
"synthetic": {"synthetic_seed": seed, "synthetic_profile": profile, "collector_version": GENERATOR_VERSION}}
|
| 981 |
+
|
| 982 |
+
|
| 983 |
+
def run_node(script, lines, *args):
|
| 984 |
+
proc = subprocess.run(["node", os.path.join(ROOT, script), *args], input="".join(json.dumps(l) + "\n" for l in lines),
|
| 985 |
+
capture_output=True, text=True, check=True)
|
| 986 |
+
return [json.loads(l) for l in proc.stdout.splitlines() if l.strip()]
|
| 987 |
+
|
| 988 |
+
|
| 989 |
def random_hostname(rng, os_family):
|
| 990 |
if os_family == "windows":
|
| 991 |
return rng.choice(["DESKTOP-", "LAPTOP-"]) + "".join(rng.choices(string.ascii_uppercase + string.digits, k=7))
|
|
|
|
| 1041 |
"status": status, "actual": actual, "expected": expected}
|
| 1042 |
if detail:
|
| 1043 |
item["detail"] = detail
|
| 1044 |
+
if check_id == "vuln.known_vulnerabilities" and status == "Pending" and actual == "matching":
|
| 1045 |
+
VULN_JOBS.append((item, device.vuln_job))
|
| 1046 |
results.append(item)
|
| 1047 |
|
| 1048 |
summary = {s: sum(1 for r in results if r["status"] == s) for s in catalog["statuses"]}
|
|
|
|
| 1064 |
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 1065 |
parser.add_argument("--count", type=int, default=100, help="number of audit runs to generate")
|
| 1066 |
parser.add_argument("--seed", type=int, default=1, help="random seed (same seed = same data)")
|
| 1067 |
+
parser.add_argument("--os", default="windows,linux,macos,ios,android", help="comma-separated OS families")
|
| 1068 |
parser.add_argument("--baseline", default=os.path.join(ROOT, "baseline.conf"))
|
| 1069 |
parser.add_argument("--out", default=os.path.join(ROOT, "synthetic", "out", "evidence.jsonl"), help="JSONL output file")
|
| 1070 |
parser.add_argument("--files", help="write one pretty JSON file per run into this folder instead of JSONL")
|
|
|
|
| 1074 |
baseline = read_conf(args.baseline)
|
| 1075 |
families = [f.strip() for f in args.os.split(",") if f.strip()]
|
| 1076 |
rng = random.Random(args.seed)
|
| 1077 |
+
records, mobile_inputs = [], []
|
| 1078 |
+
questions = mobile_questions()
|
| 1079 |
for index in range(args.count):
|
| 1080 |
record_seed = args.seed * 1_000_003 + index
|
| 1081 |
local = random.Random(record_seed)
|
| 1082 |
+
family, profile = rng.choice(families), rng.choices(list(PROFILES), weights=[2, 5, 3])[0]
|
| 1083 |
+
if family in ("ios", "android"):
|
| 1084 |
+
mobile_inputs.append((index, make_mobile_input(local, record_seed, family, profile, baseline, questions)))
|
| 1085 |
+
records.append(None)
|
| 1086 |
+
else:
|
| 1087 |
+
records.append(make_record(local, record_seed, family, profile, baseline, catalog, checks))
|
| 1088 |
+
# Phones: the same code as the mobile web page builds the evidence.
|
| 1089 |
+
if mobile_inputs:
|
| 1090 |
+
built = run_node("mobile/build-evidence.mjs", [m for _, m in mobile_inputs])
|
| 1091 |
+
for (index, _), record in zip(mobile_inputs, built):
|
| 1092 |
+
records[index] = record
|
| 1093 |
+
# Desktop known vulnerabilities: real matcher, real bundle.
|
| 1094 |
+
if VULN_JOBS:
|
| 1095 |
+
for (item, _), r in zip(VULN_JOBS, run_node("synthetic/vuln-helper.mjs", [job for _, job in VULN_JOBS])):
|
| 1096 |
+
item.update(status=r["status"], actual=r["actual"], detail=r["detail"])
|
| 1097 |
+
for record in records:
|
| 1098 |
+
record["summary"] = {s: sum(1 for r in record["results"] if r["status"] == s) for s in catalog["statuses"]}
|
| 1099 |
|
| 1100 |
if args.files:
|
| 1101 |
os.makedirs(args.files, exist_ok=True)
|
synthetic/samples/synthetic-android-42000126.json
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"schema_version": "1.0",
|
| 3 |
+
"catalog_version": "0.3.0",
|
| 4 |
+
"source": {
|
| 5 |
+
"kind": "synthetic",
|
| 6 |
+
"collector": "android",
|
| 7 |
+
"collector_version": "0.3.0",
|
| 8 |
+
"synthetic_seed": 42000126,
|
| 9 |
+
"synthetic_profile": "hardened"
|
| 10 |
+
},
|
| 11 |
+
"audit": {
|
| 12 |
+
"mode": "READ-ONLY",
|
| 13 |
+
"started_at": "2026-10-05T06:12:00+05:00",
|
| 14 |
+
"execution_user": "phone self-check (browser)",
|
| 15 |
+
"is_admin": false
|
| 16 |
+
},
|
| 17 |
+
"host": {
|
| 18 |
+
"hostname": "sales-oneplus-32",
|
| 19 |
+
"os_family": "android",
|
| 20 |
+
"os_name": "Android",
|
| 21 |
+
"os_version": "14",
|
| 22 |
+
"device_model": "OnePlus 12",
|
| 23 |
+
"browser": "Chrome 142"
|
| 24 |
+
},
|
| 25 |
+
"baseline": {
|
| 26 |
+
"PASSWORD_MIN_LENGTH": "14",
|
| 27 |
+
"PASSWORD_HISTORY": "24",
|
| 28 |
+
"PASSWORD_MAX_AGE_DAYS": "365",
|
| 29 |
+
"PASSWORD_MIN_AGE_DAYS": "1",
|
| 30 |
+
"PASSWORD_COMPLEXITY": "1",
|
| 31 |
+
"LOCKOUT_THRESHOLD_MAX": "5",
|
| 32 |
+
"LOCKOUT_DURATION_MIN_MINUTES": "15",
|
| 33 |
+
"LOCKOUT_OBSERVATION_MIN_MINUTES": "15",
|
| 34 |
+
"SIGNATURE_MAX_AGE_DAYS": "3",
|
| 35 |
+
"MACOS_XPROTECT_MAX_AGE_DAYS": "30",
|
| 36 |
+
"FIREWALL_OUTBOUND": "allow",
|
| 37 |
+
"SCREEN_LOCK_MAX_SECONDS": "300",
|
| 38 |
+
"USB_DEVICE_ACCESS": "allow",
|
| 39 |
+
"USB_READ": "allow",
|
| 40 |
+
"USB_WRITE": "deny",
|
| 41 |
+
"APPROVED_ADMINS": "itadmin",
|
| 42 |
+
"ALLOW_DISABLED_BUILTIN_ADMIN": "1",
|
| 43 |
+
"MAX_PENDING_SECURITY_UPDATES": "0",
|
| 44 |
+
"MAX_OUTDATED_APPS": "0",
|
| 45 |
+
"MAX_CRITICAL_HIGH_VULNERABILITIES": "0",
|
| 46 |
+
"UPDATES_MAX_AGE_DAYS": "30",
|
| 47 |
+
"BACKUP_MAX_AGE_DAYS": "7",
|
| 48 |
+
"APPROVED_REMOTE_SERVICES": "",
|
| 49 |
+
"WINDOWS_MIN_BUILD": "22631",
|
| 50 |
+
"MACOS_MIN_MAJOR": "14",
|
| 51 |
+
"UBUNTU_MIN_VERSION": "22.04",
|
| 52 |
+
"DEBIAN_MIN_VERSION": "12",
|
| 53 |
+
"RHEL_MIN_VERSION": "9",
|
| 54 |
+
"PERSONAL_DATA_SCAN": "0",
|
| 55 |
+
"APPROVED_AI_TOOLS": "",
|
| 56 |
+
"IOS_MIN_MAJOR": "18",
|
| 57 |
+
"ANDROID_MIN_MAJOR": "13",
|
| 58 |
+
"MOBILE_PATCH_MAX_AGE_DAYS": "90",
|
| 59 |
+
"MOBILE_PASSCODE_MIN_DIGITS": "6"
|
| 60 |
+
},
|
| 61 |
+
"results": [
|
| 62 |
+
{
|
| 63 |
+
"id": "malware.antivirus",
|
| 64 |
+
"section": "baseline",
|
| 65 |
+
"name": "Google Play Protect",
|
| 66 |
+
"status": "Compliant",
|
| 67 |
+
"actual": "Play Protect scanning on",
|
| 68 |
+
"expected": "on",
|
| 69 |
+
"evidence": "self-reported"
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
"id": "screen_lock.enabled",
|
| 73 |
+
"section": "baseline",
|
| 74 |
+
"name": "Screen lock",
|
| 75 |
+
"status": "Compliant",
|
| 76 |
+
"actual": "screen lock set",
|
| 77 |
+
"expected": "PIN, password or pattern",
|
| 78 |
+
"detail": "The browser reports that a device unlock method (passcode, PIN, Face ID or fingerprint) is set up.",
|
| 79 |
+
"evidence": "detected"
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
"id": "screen_lock.timeout",
|
| 83 |
+
"section": "baseline",
|
| 84 |
+
"name": "Screen timeout",
|
| 85 |
+
"status": "Compliant",
|
| 86 |
+
"actual": "Screen timeout 2 minutes",
|
| 87 |
+
"expected": "<= 300 seconds",
|
| 88 |
+
"evidence": "self-reported"
|
| 89 |
+
},
|
| 90 |
+
{
|
| 91 |
+
"id": "mobile.passcode_strength",
|
| 92 |
+
"section": "baseline",
|
| 93 |
+
"name": "Screen lock strength",
|
| 94 |
+
"status": "Compliant",
|
| 95 |
+
"actual": "6-digit PIN",
|
| 96 |
+
"expected": ">= 6 digits or alphanumeric",
|
| 97 |
+
"evidence": "self-reported"
|
| 98 |
+
},
|
| 99 |
+
{
|
| 100 |
+
"id": "mobile.jailbreak_root",
|
| 101 |
+
"section": "baseline",
|
| 102 |
+
"name": "Not rooted",
|
| 103 |
+
"status": "Compliant",
|
| 104 |
+
"actual": "not rooted (user confirmed)",
|
| 105 |
+
"expected": "not rooted",
|
| 106 |
+
"evidence": "self-reported"
|
| 107 |
+
},
|
| 108 |
+
{
|
| 109 |
+
"id": "mobile.app_sources",
|
| 110 |
+
"section": "baseline",
|
| 111 |
+
"name": "Install unknown apps blocked",
|
| 112 |
+
"status": "Compliant",
|
| 113 |
+
"actual": "unknown apps blocked for all apps",
|
| 114 |
+
"expected": "all apps: Not allowed",
|
| 115 |
+
"evidence": "self-reported"
|
| 116 |
+
},
|
| 117 |
+
{
|
| 118 |
+
"id": "mobile.usb_debugging",
|
| 119 |
+
"section": "baseline",
|
| 120 |
+
"name": "USB debugging off",
|
| 121 |
+
"status": "Compliant",
|
| 122 |
+
"actual": "off",
|
| 123 |
+
"expected": "off",
|
| 124 |
+
"evidence": "self-reported"
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"id": "mobile.find_my_device",
|
| 128 |
+
"section": "baseline",
|
| 129 |
+
"name": "Find My Device",
|
| 130 |
+
"status": "Compliant",
|
| 131 |
+
"actual": "on",
|
| 132 |
+
"expected": "on",
|
| 133 |
+
"evidence": "self-reported"
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
"id": "encryption.enabled",
|
| 137 |
+
"section": "baseline",
|
| 138 |
+
"name": "Device encryption",
|
| 139 |
+
"status": "Compliant",
|
| 140 |
+
"actual": "on (Android 14 encrypts by default)",
|
| 141 |
+
"expected": "on",
|
| 142 |
+
"evidence": "detected"
|
| 143 |
+
},
|
| 144 |
+
{
|
| 145 |
+
"id": "vuln.pending_updates",
|
| 146 |
+
"section": "baseline",
|
| 147 |
+
"name": "Android security update",
|
| 148 |
+
"status": "Compliant",
|
| 149 |
+
"actual": "security patch 2026-10 (0 months old)",
|
| 150 |
+
"expected": "<= 90 days old",
|
| 151 |
+
"evidence": "self-reported"
|
| 152 |
+
},
|
| 153 |
+
{
|
| 154 |
+
"id": "vuln.outdated_apps",
|
| 155 |
+
"section": "baseline",
|
| 156 |
+
"name": "App updates",
|
| 157 |
+
"status": "Compliant",
|
| 158 |
+
"actual": "automatic app updates on",
|
| 159 |
+
"expected": "automatic app updates on",
|
| 160 |
+
"evidence": "self-reported"
|
| 161 |
+
},
|
| 162 |
+
{
|
| 163 |
+
"id": "gdpr.auto_updates",
|
| 164 |
+
"section": "gdpr",
|
| 165 |
+
"name": "Automatic system updates",
|
| 166 |
+
"status": "Compliant",
|
| 167 |
+
"actual": "automatic",
|
| 168 |
+
"expected": "automatic",
|
| 169 |
+
"evidence": "self-reported"
|
| 170 |
+
},
|
| 171 |
+
{
|
| 172 |
+
"id": "gdpr.os_supported",
|
| 173 |
+
"section": "gdpr",
|
| 174 |
+
"name": "Supported operating system version",
|
| 175 |
+
"status": "Compliant",
|
| 176 |
+
"actual": "Android 14",
|
| 177 |
+
"expected": ">= Android 13",
|
| 178 |
+
"detail": "Version reported by the browser (Chrome 142).",
|
| 179 |
+
"evidence": "detected"
|
| 180 |
+
},
|
| 181 |
+
{
|
| 182 |
+
"id": "gdpr.time_sync",
|
| 183 |
+
"section": "gdpr",
|
| 184 |
+
"name": "Date & time set automatically",
|
| 185 |
+
"status": "Compliant",
|
| 186 |
+
"actual": "set automatically",
|
| 187 |
+
"expected": "set automatically",
|
| 188 |
+
"evidence": "self-reported"
|
| 189 |
+
},
|
| 190 |
+
{
|
| 191 |
+
"id": "gdpr.backup_configured",
|
| 192 |
+
"section": "gdpr",
|
| 193 |
+
"name": "Google backup",
|
| 194 |
+
"status": "Compliant",
|
| 195 |
+
"actual": "phone maker cloud backup on",
|
| 196 |
+
"expected": "backup on",
|
| 197 |
+
"evidence": "self-reported"
|
| 198 |
+
},
|
| 199 |
+
{
|
| 200 |
+
"id": "gdpr.telemetry",
|
| 201 |
+
"section": "gdpr",
|
| 202 |
+
"name": "Usage & diagnostics sharing minimised",
|
| 203 |
+
"status": "Compliant",
|
| 204 |
+
"actual": "usage & diagnostics off",
|
| 205 |
+
"expected": "usage & diagnostics off",
|
| 206 |
+
"evidence": "self-reported"
|
| 207 |
+
},
|
| 208 |
+
{
|
| 209 |
+
"id": "gdpr.lock_screen_previews",
|
| 210 |
+
"section": "gdpr",
|
| 211 |
+
"name": "Sensitive notifications hidden when locked",
|
| 212 |
+
"status": "Compliant",
|
| 213 |
+
"actual": "no notifications on lock screen",
|
| 214 |
+
"expected": "sensitive content hidden",
|
| 215 |
+
"evidence": "self-reported"
|
| 216 |
+
},
|
| 217 |
+
{
|
| 218 |
+
"id": "aiact.inventory",
|
| 219 |
+
"section": "aiact",
|
| 220 |
+
"name": "AI software inventory",
|
| 221 |
+
"status": "Compliant",
|
| 222 |
+
"actual": "ChatGPT, Gemini, Perplexity",
|
| 223 |
+
"expected": "inventory taken",
|
| 224 |
+
"detail": "AI apps listed by the user.",
|
| 225 |
+
"evidence": "self-reported"
|
| 226 |
+
},
|
| 227 |
+
{
|
| 228 |
+
"id": "aiact.unapproved",
|
| 229 |
+
"section": "aiact",
|
| 230 |
+
"name": "Unapproved AI tools (shadow AI)",
|
| 231 |
+
"status": "Non-Compliant",
|
| 232 |
+
"actual": "ChatGPT, Gemini, Perplexity",
|
| 233 |
+
"expected": "only: none",
|
| 234 |
+
"evidence": "self-reported"
|
| 235 |
+
},
|
| 236 |
+
{
|
| 237 |
+
"id": "aiact.risk_classification",
|
| 238 |
+
"section": "aiact",
|
| 239 |
+
"name": "AI system risk classification",
|
| 240 |
+
"status": "Pending",
|
| 241 |
+
"actual": "ChatGPT, Gemini, Perplexity",
|
| 242 |
+
"expected": "every AI tool classified",
|
| 243 |
+
"detail": "Classified by the auditor model (Caveat classifier) in a later step.",
|
| 244 |
+
"evidence": "self-reported"
|
| 245 |
+
},
|
| 246 |
+
{
|
| 247 |
+
"id": "aiact.os_ai_features",
|
| 248 |
+
"section": "aiact",
|
| 249 |
+
"name": "AI assistant access restricted",
|
| 250 |
+
"status": "Pending",
|
| 251 |
+
"actual": "not answered",
|
| 252 |
+
"expected": "screen access off unless approved",
|
| 253 |
+
"detail": "The user could not find or confirm this setting.",
|
| 254 |
+
"evidence": "self-reported"
|
| 255 |
+
},
|
| 256 |
+
{
|
| 257 |
+
"id": "aiact.log_retention",
|
| 258 |
+
"section": "aiact",
|
| 259 |
+
"name": "AI usage log retention",
|
| 260 |
+
"status": "Pending",
|
| 261 |
+
"actual": "depends on classification",
|
| 262 |
+
"expected": ">= 6 months for high-risk AI",
|
| 263 |
+
"detail": "Required only if a tool is classified high-risk.",
|
| 264 |
+
"evidence": "self-reported"
|
| 265 |
+
},
|
| 266 |
+
{
|
| 267 |
+
"id": "org.ropa",
|
| 268 |
+
"section": "org",
|
| 269 |
+
"name": "Record of processing activities",
|
| 270 |
+
"status": "Pending",
|
| 271 |
+
"actual": "not answered",
|
| 272 |
+
"expected": "yes",
|
| 273 |
+
"detail": "Answered once by the organisation (organisational.conf); not part of the phone self-check."
|
| 274 |
+
},
|
| 275 |
+
{
|
| 276 |
+
"id": "org.dpia",
|
| 277 |
+
"section": "org",
|
| 278 |
+
"name": "Data protection impact assessment",
|
| 279 |
+
"status": "Pending",
|
| 280 |
+
"actual": "not answered",
|
| 281 |
+
"expected": "yes",
|
| 282 |
+
"detail": "Answered once by the organisation (organisational.conf); not part of the phone self-check."
|
| 283 |
+
},
|
| 284 |
+
{
|
| 285 |
+
"id": "org.breach_procedure",
|
| 286 |
+
"section": "org",
|
| 287 |
+
"name": "Breach response procedure",
|
| 288 |
+
"status": "Pending",
|
| 289 |
+
"actual": "not answered",
|
| 290 |
+
"expected": "yes",
|
| 291 |
+
"detail": "Answered once by the organisation (organisational.conf); not part of the phone self-check."
|
| 292 |
+
},
|
| 293 |
+
{
|
| 294 |
+
"id": "org.processor_agreements",
|
| 295 |
+
"section": "org",
|
| 296 |
+
"name": "Data processing agreements",
|
| 297 |
+
"status": "Pending",
|
| 298 |
+
"actual": "not answered",
|
| 299 |
+
"expected": "yes",
|
| 300 |
+
"detail": "Answered once by the organisation (organisational.conf); not part of the phone self-check."
|
| 301 |
+
},
|
| 302 |
+
{
|
| 303 |
+
"id": "org.ai_register",
|
| 304 |
+
"section": "org",
|
| 305 |
+
"name": "AI register",
|
| 306 |
+
"status": "Pending",
|
| 307 |
+
"actual": "not answered",
|
| 308 |
+
"expected": "yes",
|
| 309 |
+
"detail": "Answered once by the organisation (organisational.conf); not part of the phone self-check."
|
| 310 |
+
},
|
| 311 |
+
{
|
| 312 |
+
"id": "org.ai_literacy",
|
| 313 |
+
"section": "org",
|
| 314 |
+
"name": "AI literacy training",
|
| 315 |
+
"status": "Pending",
|
| 316 |
+
"actual": "not answered",
|
| 317 |
+
"expected": "yes",
|
| 318 |
+
"detail": "Answered once by the organisation (organisational.conf); not part of the phone self-check."
|
| 319 |
+
},
|
| 320 |
+
{
|
| 321 |
+
"id": "org.human_oversight",
|
| 322 |
+
"section": "org",
|
| 323 |
+
"name": "Human oversight for high-risk AI",
|
| 324 |
+
"status": "Pending",
|
| 325 |
+
"actual": "not answered",
|
| 326 |
+
"expected": "yes",
|
| 327 |
+
"detail": "Answered once by the organisation (organisational.conf); not part of the phone self-check."
|
| 328 |
+
},
|
| 329 |
+
{
|
| 330 |
+
"id": "org.ai_transparency",
|
| 331 |
+
"section": "org",
|
| 332 |
+
"name": "AI transparency to users",
|
| 333 |
+
"status": "Pending",
|
| 334 |
+
"actual": "not answered",
|
| 335 |
+
"expected": "yes",
|
| 336 |
+
"detail": "Answered once by the organisation (organisational.conf); not part of the phone self-check."
|
| 337 |
+
}
|
| 338 |
+
],
|
| 339 |
+
"summary": {
|
| 340 |
+
"Compliant": 18,
|
| 341 |
+
"Non-Compliant": 1,
|
| 342 |
+
"NotApplicable": 0,
|
| 343 |
+
"Pending": 11,
|
| 344 |
+
"Error": 0
|
| 345 |
+
}
|
| 346 |
+
}
|
synthetic/samples/synthetic-ios-42000126.json
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"schema_version": "1.0",
|
| 3 |
+
"catalog_version": "0.3.0",
|
| 4 |
+
"source": {
|
| 5 |
+
"kind": "synthetic",
|
| 6 |
+
"collector": "ios",
|
| 7 |
+
"collector_version": "0.3.0",
|
| 8 |
+
"synthetic_seed": 42000126,
|
| 9 |
+
"synthetic_profile": "hardened"
|
| 10 |
+
},
|
| 11 |
+
"audit": {
|
| 12 |
+
"mode": "READ-ONLY",
|
| 13 |
+
"started_at": "2026-10-05T06:12:00+05:00",
|
| 14 |
+
"execution_user": "phone self-check (browser)",
|
| 15 |
+
"is_admin": false
|
| 16 |
+
},
|
| 17 |
+
"host": {
|
| 18 |
+
"hostname": "SALES-iPhone-39",
|
| 19 |
+
"os_family": "ios",
|
| 20 |
+
"os_name": "iOS",
|
| 21 |
+
"os_version": "26.7",
|
| 22 |
+
"device_model": "iPhone",
|
| 23 |
+
"browser": "Safari 26.7"
|
| 24 |
+
},
|
| 25 |
+
"baseline": {
|
| 26 |
+
"PASSWORD_MIN_LENGTH": "14",
|
| 27 |
+
"PASSWORD_HISTORY": "24",
|
| 28 |
+
"PASSWORD_MAX_AGE_DAYS": "365",
|
| 29 |
+
"PASSWORD_MIN_AGE_DAYS": "1",
|
| 30 |
+
"PASSWORD_COMPLEXITY": "1",
|
| 31 |
+
"LOCKOUT_THRESHOLD_MAX": "5",
|
| 32 |
+
"LOCKOUT_DURATION_MIN_MINUTES": "15",
|
| 33 |
+
"LOCKOUT_OBSERVATION_MIN_MINUTES": "15",
|
| 34 |
+
"SIGNATURE_MAX_AGE_DAYS": "3",
|
| 35 |
+
"MACOS_XPROTECT_MAX_AGE_DAYS": "30",
|
| 36 |
+
"FIREWALL_OUTBOUND": "allow",
|
| 37 |
+
"SCREEN_LOCK_MAX_SECONDS": "300",
|
| 38 |
+
"USB_DEVICE_ACCESS": "allow",
|
| 39 |
+
"USB_READ": "allow",
|
| 40 |
+
"USB_WRITE": "deny",
|
| 41 |
+
"APPROVED_ADMINS": "itadmin",
|
| 42 |
+
"ALLOW_DISABLED_BUILTIN_ADMIN": "1",
|
| 43 |
+
"MAX_PENDING_SECURITY_UPDATES": "0",
|
| 44 |
+
"MAX_OUTDATED_APPS": "0",
|
| 45 |
+
"MAX_CRITICAL_HIGH_VULNERABILITIES": "0",
|
| 46 |
+
"UPDATES_MAX_AGE_DAYS": "30",
|
| 47 |
+
"BACKUP_MAX_AGE_DAYS": "7",
|
| 48 |
+
"APPROVED_REMOTE_SERVICES": "",
|
| 49 |
+
"WINDOWS_MIN_BUILD": "22631",
|
| 50 |
+
"MACOS_MIN_MAJOR": "14",
|
| 51 |
+
"UBUNTU_MIN_VERSION": "22.04",
|
| 52 |
+
"DEBIAN_MIN_VERSION": "12",
|
| 53 |
+
"RHEL_MIN_VERSION": "9",
|
| 54 |
+
"PERSONAL_DATA_SCAN": "0",
|
| 55 |
+
"APPROVED_AI_TOOLS": "",
|
| 56 |
+
"IOS_MIN_MAJOR": "18",
|
| 57 |
+
"ANDROID_MIN_MAJOR": "13",
|
| 58 |
+
"MOBILE_PATCH_MAX_AGE_DAYS": "90",
|
| 59 |
+
"MOBILE_PASSCODE_MIN_DIGITS": "6"
|
| 60 |
+
},
|
| 61 |
+
"results": [
|
| 62 |
+
{
|
| 63 |
+
"id": "screen_lock.enabled",
|
| 64 |
+
"section": "baseline",
|
| 65 |
+
"name": "Passcode",
|
| 66 |
+
"status": "Compliant",
|
| 67 |
+
"actual": "passcode set",
|
| 68 |
+
"expected": "passcode set",
|
| 69 |
+
"evidence": "self-reported"
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
"id": "screen_lock.timeout",
|
| 73 |
+
"section": "baseline",
|
| 74 |
+
"name": "Auto-Lock",
|
| 75 |
+
"status": "Compliant",
|
| 76 |
+
"actual": "Auto-Lock 5 minutes",
|
| 77 |
+
"expected": "<= 300 seconds",
|
| 78 |
+
"evidence": "self-reported"
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
"id": "mobile.passcode_strength",
|
| 82 |
+
"section": "baseline",
|
| 83 |
+
"name": "Passcode strength",
|
| 84 |
+
"status": "Compliant",
|
| 85 |
+
"actual": "alphanumeric",
|
| 86 |
+
"expected": ">= 6 digits or alphanumeric",
|
| 87 |
+
"evidence": "self-reported"
|
| 88 |
+
},
|
| 89 |
+
{
|
| 90 |
+
"id": "mobile.jailbreak_root",
|
| 91 |
+
"section": "baseline",
|
| 92 |
+
"name": "Not jailbroken",
|
| 93 |
+
"status": "Compliant",
|
| 94 |
+
"actual": "not jailbroken (user confirmed)",
|
| 95 |
+
"expected": "not jailbroken",
|
| 96 |
+
"evidence": "self-reported"
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"id": "mobile.app_sources",
|
| 100 |
+
"section": "baseline",
|
| 101 |
+
"name": "Apps only from the App Store",
|
| 102 |
+
"status": "Compliant",
|
| 103 |
+
"actual": "App Store only",
|
| 104 |
+
"expected": "App Store only",
|
| 105 |
+
"evidence": "self-reported"
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
"id": "mobile.find_my_device",
|
| 109 |
+
"section": "baseline",
|
| 110 |
+
"name": "Find My iPhone",
|
| 111 |
+
"status": "Compliant",
|
| 112 |
+
"actual": "on",
|
| 113 |
+
"expected": "on",
|
| 114 |
+
"evidence": "self-reported"
|
| 115 |
+
},
|
| 116 |
+
{
|
| 117 |
+
"id": "encryption.enabled",
|
| 118 |
+
"section": "baseline",
|
| 119 |
+
"name": "Data protection (encryption)",
|
| 120 |
+
"status": "Compliant",
|
| 121 |
+
"actual": "on (passcode set)",
|
| 122 |
+
"expected": "on",
|
| 123 |
+
"detail": "iOS encrypts the data on the phone with hardware keys once a passcode is set.",
|
| 124 |
+
"evidence": "self-reported"
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"id": "vuln.pending_updates",
|
| 128 |
+
"section": "baseline",
|
| 129 |
+
"name": "iOS security updates",
|
| 130 |
+
"status": "Non-Compliant",
|
| 131 |
+
"actual": "iOS 26.7 (update to 26.7.1 available)",
|
| 132 |
+
"expected": "latest iOS security release",
|
| 133 |
+
"detail": "Install iOS 26.7.1 or later in Settings › General › Software Update.",
|
| 134 |
+
"evidence": "detected"
|
| 135 |
+
},
|
| 136 |
+
{
|
| 137 |
+
"id": "vuln.outdated_apps",
|
| 138 |
+
"section": "baseline",
|
| 139 |
+
"name": "App updates",
|
| 140 |
+
"status": "Compliant",
|
| 141 |
+
"actual": "automatic app updates on",
|
| 142 |
+
"expected": "automatic app updates on",
|
| 143 |
+
"evidence": "self-reported"
|
| 144 |
+
},
|
| 145 |
+
{
|
| 146 |
+
"id": "vuln.known_vulnerabilities",
|
| 147 |
+
"section": "baseline",
|
| 148 |
+
"name": "Known vulnerabilities in iOS",
|
| 149 |
+
"status": "Non-Compliant",
|
| 150 |
+
"actual": "15 critical/high",
|
| 151 |
+
"expected": "<= 0 critical/high",
|
| 152 |
+
"detail": "Matched against offline OSV/NVD data from 2026-10-08. iOS 26.7 checked against NVD iOS records. iOS 26.7 (15 CVEs, critical, actively exploited; fixed in 27.0; e.g. CVE-2026-20683, CVE-2026-43688). 32 medium/low. Patch-level versions (x.y.z) are not always visible to the browser; confirm the version in Settings › General › About.",
|
| 153 |
+
"evidence": "detected"
|
| 154 |
+
},
|
| 155 |
+
{
|
| 156 |
+
"id": "gdpr.auto_updates",
|
| 157 |
+
"section": "gdpr",
|
| 158 |
+
"name": "Automatic iOS updates",
|
| 159 |
+
"status": "Compliant",
|
| 160 |
+
"actual": "download and install on",
|
| 161 |
+
"expected": "download and install on",
|
| 162 |
+
"evidence": "self-reported"
|
| 163 |
+
},
|
| 164 |
+
{
|
| 165 |
+
"id": "gdpr.os_supported",
|
| 166 |
+
"section": "gdpr",
|
| 167 |
+
"name": "Supported operating system version",
|
| 168 |
+
"status": "Compliant",
|
| 169 |
+
"actual": "iOS 26.7",
|
| 170 |
+
"expected": ">= iOS 18",
|
| 171 |
+
"detail": "Version reported by the browser (Safari 26.7).",
|
| 172 |
+
"evidence": "detected"
|
| 173 |
+
},
|
| 174 |
+
{
|
| 175 |
+
"id": "gdpr.time_sync",
|
| 176 |
+
"section": "gdpr",
|
| 177 |
+
"name": "Date & time set automatically",
|
| 178 |
+
"status": "Compliant",
|
| 179 |
+
"actual": "set automatically",
|
| 180 |
+
"expected": "set automatically",
|
| 181 |
+
"evidence": "self-reported"
|
| 182 |
+
},
|
| 183 |
+
{
|
| 184 |
+
"id": "gdpr.backup_configured",
|
| 185 |
+
"section": "gdpr",
|
| 186 |
+
"name": "iCloud Backup",
|
| 187 |
+
"status": "Compliant",
|
| 188 |
+
"actual": "backed up to a computer",
|
| 189 |
+
"expected": "backup on",
|
| 190 |
+
"detail": "Manual computer backups depend on the user remembering them.",
|
| 191 |
+
"evidence": "self-reported"
|
| 192 |
+
},
|
| 193 |
+
{
|
| 194 |
+
"id": "gdpr.telemetry",
|
| 195 |
+
"section": "gdpr",
|
| 196 |
+
"name": "Analytics sharing minimised",
|
| 197 |
+
"status": "Compliant",
|
| 198 |
+
"actual": "analytics sharing off",
|
| 199 |
+
"expected": "analytics sharing off",
|
| 200 |
+
"evidence": "self-reported"
|
| 201 |
+
},
|
| 202 |
+
{
|
| 203 |
+
"id": "gdpr.lock_screen_previews",
|
| 204 |
+
"section": "gdpr",
|
| 205 |
+
"name": "Notification previews hidden when locked",
|
| 206 |
+
"status": "Compliant",
|
| 207 |
+
"actual": "Never",
|
| 208 |
+
"expected": "When Unlocked or Never",
|
| 209 |
+
"evidence": "self-reported"
|
| 210 |
+
},
|
| 211 |
+
{
|
| 212 |
+
"id": "aiact.inventory",
|
| 213 |
+
"section": "aiact",
|
| 214 |
+
"name": "AI software inventory",
|
| 215 |
+
"status": "Compliant",
|
| 216 |
+
"actual": "ChatGPT, Microsoft Copilot",
|
| 217 |
+
"expected": "inventory taken",
|
| 218 |
+
"detail": "AI apps listed by the user.",
|
| 219 |
+
"evidence": "self-reported"
|
| 220 |
+
},
|
| 221 |
+
{
|
| 222 |
+
"id": "aiact.unapproved",
|
| 223 |
+
"section": "aiact",
|
| 224 |
+
"name": "Unapproved AI tools (shadow AI)",
|
| 225 |
+
"status": "Non-Compliant",
|
| 226 |
+
"actual": "ChatGPT, Microsoft Copilot",
|
| 227 |
+
"expected": "only: none",
|
| 228 |
+
"evidence": "self-reported"
|
| 229 |
+
},
|
| 230 |
+
{
|
| 231 |
+
"id": "aiact.risk_classification",
|
| 232 |
+
"section": "aiact",
|
| 233 |
+
"name": "AI system risk classification",
|
| 234 |
+
"status": "Pending",
|
| 235 |
+
"actual": "ChatGPT, Microsoft Copilot",
|
| 236 |
+
"expected": "every AI tool classified",
|
| 237 |
+
"detail": "Classified by the auditor model (Caveat classifier) in a later step.",
|
| 238 |
+
"evidence": "self-reported"
|
| 239 |
+
},
|
| 240 |
+
{
|
| 241 |
+
"id": "aiact.os_ai_features",
|
| 242 |
+
"section": "aiact",
|
| 243 |
+
"name": "Apple Intelligence external integrations restricted",
|
| 244 |
+
"status": "NotApplicable",
|
| 245 |
+
"actual": "Apple Intelligence not available",
|
| 246 |
+
"expected": "external AI off unless approved",
|
| 247 |
+
"evidence": "self-reported"
|
| 248 |
+
},
|
| 249 |
+
{
|
| 250 |
+
"id": "aiact.log_retention",
|
| 251 |
+
"section": "aiact",
|
| 252 |
+
"name": "AI usage log retention",
|
| 253 |
+
"status": "Pending",
|
| 254 |
+
"actual": "depends on classification",
|
| 255 |
+
"expected": ">= 6 months for high-risk AI",
|
| 256 |
+
"detail": "Required only if a tool is classified high-risk.",
|
| 257 |
+
"evidence": "self-reported"
|
| 258 |
+
},
|
| 259 |
+
{
|
| 260 |
+
"id": "org.ropa",
|
| 261 |
+
"section": "org",
|
| 262 |
+
"name": "Record of processing activities",
|
| 263 |
+
"status": "Compliant",
|
| 264 |
+
"actual": "yes",
|
| 265 |
+
"expected": "yes"
|
| 266 |
+
},
|
| 267 |
+
{
|
| 268 |
+
"id": "org.dpia",
|
| 269 |
+
"section": "org",
|
| 270 |
+
"name": "Data protection impact assessment",
|
| 271 |
+
"status": "Compliant",
|
| 272 |
+
"actual": "yes",
|
| 273 |
+
"expected": "yes"
|
| 274 |
+
},
|
| 275 |
+
{
|
| 276 |
+
"id": "org.breach_procedure",
|
| 277 |
+
"section": "org",
|
| 278 |
+
"name": "Breach response procedure",
|
| 279 |
+
"status": "Compliant",
|
| 280 |
+
"actual": "yes",
|
| 281 |
+
"expected": "yes"
|
| 282 |
+
},
|
| 283 |
+
{
|
| 284 |
+
"id": "org.processor_agreements",
|
| 285 |
+
"section": "org",
|
| 286 |
+
"name": "Data processing agreements",
|
| 287 |
+
"status": "Compliant",
|
| 288 |
+
"actual": "yes",
|
| 289 |
+
"expected": "yes"
|
| 290 |
+
},
|
| 291 |
+
{
|
| 292 |
+
"id": "org.ai_register",
|
| 293 |
+
"section": "org",
|
| 294 |
+
"name": "AI register",
|
| 295 |
+
"status": "Non-Compliant",
|
| 296 |
+
"actual": "partial",
|
| 297 |
+
"expected": "yes"
|
| 298 |
+
},
|
| 299 |
+
{
|
| 300 |
+
"id": "org.ai_literacy",
|
| 301 |
+
"section": "org",
|
| 302 |
+
"name": "AI literacy training",
|
| 303 |
+
"status": "Compliant",
|
| 304 |
+
"actual": "yes",
|
| 305 |
+
"expected": "yes"
|
| 306 |
+
},
|
| 307 |
+
{
|
| 308 |
+
"id": "org.human_oversight",
|
| 309 |
+
"section": "org",
|
| 310 |
+
"name": "Human oversight for high-risk AI",
|
| 311 |
+
"status": "Compliant",
|
| 312 |
+
"actual": "yes",
|
| 313 |
+
"expected": "yes"
|
| 314 |
+
},
|
| 315 |
+
{
|
| 316 |
+
"id": "org.ai_transparency",
|
| 317 |
+
"section": "org",
|
| 318 |
+
"name": "AI transparency to users",
|
| 319 |
+
"status": "Compliant",
|
| 320 |
+
"actual": "yes",
|
| 321 |
+
"expected": "yes"
|
| 322 |
+
}
|
| 323 |
+
],
|
| 324 |
+
"summary": {
|
| 325 |
+
"Compliant": 22,
|
| 326 |
+
"Non-Compliant": 4,
|
| 327 |
+
"NotApplicable": 1,
|
| 328 |
+
"Pending": 2,
|
| 329 |
+
"Error": 0
|
| 330 |
+
}
|
| 331 |
+
}
|
synthetic/samples/synthetic-linux-42000126.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
{
|
| 2 |
"schema_version": "1.0",
|
| 3 |
-
"catalog_version": "0.
|
| 4 |
"source": {
|
| 5 |
"kind": "synthetic",
|
| 6 |
"collector": "linux",
|
| 7 |
-
"collector_version": "0.
|
| 8 |
"synthetic_seed": 42000126,
|
| 9 |
"synthetic_profile": "hardened"
|
| 10 |
},
|
|
@@ -53,7 +53,11 @@
|
|
| 53 |
"DEBIAN_MIN_VERSION": "12",
|
| 54 |
"RHEL_MIN_VERSION": "9",
|
| 55 |
"PERSONAL_DATA_SCAN": "0",
|
| 56 |
-
"APPROVED_AI_TOOLS": ""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
},
|
| 58 |
"results": [
|
| 59 |
{
|
|
@@ -290,18 +294,18 @@
|
|
| 290 |
"section": "baseline",
|
| 291 |
"name": "Pending security updates",
|
| 292 |
"status": "Non-Compliant",
|
| 293 |
-
"actual": "
|
| 294 |
"expected": "<= 0 pending",
|
| 295 |
-
"detail": "Including:
|
| 296 |
},
|
| 297 |
{
|
| 298 |
"id": "vuln.outdated_apps",
|
| 299 |
"section": "baseline",
|
| 300 |
"name": "Outdated third-party applications",
|
| 301 |
-
"status": "
|
| 302 |
-
"actual": "
|
| 303 |
"expected": "<= 0 outdated",
|
| 304 |
-
"detail": "
|
| 305 |
},
|
| 306 |
{
|
| 307 |
"id": "vuln.known_vulnerabilities",
|
|
@@ -310,7 +314,7 @@
|
|
| 310 |
"status": "Compliant",
|
| 311 |
"actual": "0 critical/high",
|
| 312 |
"expected": "<= 0 critical/high",
|
| 313 |
-
"detail": "Matched against offline OSV/NVD data from 2026-
|
| 314 |
},
|
| 315 |
{
|
| 316 |
"id": "gdpr.auto_updates",
|
|
@@ -325,9 +329,9 @@
|
|
| 325 |
"section": "gdpr",
|
| 326 |
"name": "Security updates installed recently",
|
| 327 |
"status": "Compliant",
|
| 328 |
-
"actual": "
|
| 329 |
"expected": "<= 30 days ago",
|
| 330 |
-
"detail": "Last package upgrade: 2026-09-
|
| 331 |
},
|
| 332 |
{
|
| 333 |
"id": "gdpr.os_supported",
|
|
@@ -358,17 +362,16 @@
|
|
| 358 |
"section": "gdpr",
|
| 359 |
"name": "Backup configured",
|
| 360 |
"status": "Compliant",
|
| 361 |
-
"actual": "
|
| 362 |
"expected": "backup configured"
|
| 363 |
},
|
| 364 |
{
|
| 365 |
"id": "gdpr.backup_recent",
|
| 366 |
"section": "gdpr",
|
| 367 |
"name": "Recent backup",
|
| 368 |
-
"status": "
|
| 369 |
-
"actual": "
|
| 370 |
-
"expected": "<= 7 days ago"
|
| 371 |
-
"detail": "Confirm the last backup date in: restic-backup.timer."
|
| 372 |
},
|
| 373 |
{
|
| 374 |
"id": "gdpr.removable_encryption",
|
|
@@ -416,26 +419,24 @@
|
|
| 416 |
"section": "aiact",
|
| 417 |
"name": "AI software inventory",
|
| 418 |
"status": "Compliant",
|
| 419 |
-
"actual": "
|
| 420 |
-
"expected": "inventory taken"
|
| 421 |
-
"detail": "Local models: /home/jdoe/.cache/huggingface/hub (17G)."
|
| 422 |
},
|
| 423 |
{
|
| 424 |
"id": "aiact.unapproved",
|
| 425 |
"section": "aiact",
|
| 426 |
"name": "Unapproved AI tools (shadow AI)",
|
| 427 |
-
"status": "
|
| 428 |
-
"actual": "
|
| 429 |
"expected": "only: none"
|
| 430 |
},
|
| 431 |
{
|
| 432 |
"id": "aiact.risk_classification",
|
| 433 |
"section": "aiact",
|
| 434 |
"name": "AI system risk classification",
|
| 435 |
-
"status": "
|
| 436 |
-
"actual": "
|
| 437 |
-
"expected": "every AI tool classified"
|
| 438 |
-
"detail": "Classified by the auditor model (Caveat classifier) in a later step."
|
| 439 |
},
|
| 440 |
{
|
| 441 |
"id": "aiact.os_ai_features",
|
|
@@ -450,10 +451,9 @@
|
|
| 450 |
"id": "aiact.log_retention",
|
| 451 |
"section": "aiact",
|
| 452 |
"name": "AI usage log retention",
|
| 453 |
-
"status": "
|
| 454 |
-
"actual": "
|
| 455 |
-
"expected": ">= 6 months for high-risk AI"
|
| 456 |
-
"detail": "Required only if a tool is classified high-risk."
|
| 457 |
},
|
| 458 |
{
|
| 459 |
"id": "org.ropa",
|
|
@@ -491,8 +491,8 @@
|
|
| 491 |
"id": "org.ai_register",
|
| 492 |
"section": "org",
|
| 493 |
"name": "AI register",
|
| 494 |
-
"status": "Compliant",
|
| 495 |
-
"actual": "
|
| 496 |
"expected": "yes"
|
| 497 |
},
|
| 498 |
{
|
|
@@ -515,16 +515,16 @@
|
|
| 515 |
"id": "org.ai_transparency",
|
| 516 |
"section": "org",
|
| 517 |
"name": "AI transparency to users",
|
| 518 |
-
"status": "
|
| 519 |
-
"actual": "
|
| 520 |
"expected": "yes"
|
| 521 |
}
|
| 522 |
],
|
| 523 |
"summary": {
|
| 524 |
-
"Compliant":
|
| 525 |
-
"Non-Compliant":
|
| 526 |
-
"NotApplicable":
|
| 527 |
-
"Pending":
|
| 528 |
"Error": 0
|
| 529 |
}
|
| 530 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"schema_version": "1.0",
|
| 3 |
+
"catalog_version": "0.3.0",
|
| 4 |
"source": {
|
| 5 |
"kind": "synthetic",
|
| 6 |
"collector": "linux",
|
| 7 |
+
"collector_version": "0.3.0",
|
| 8 |
"synthetic_seed": 42000126,
|
| 9 |
"synthetic_profile": "hardened"
|
| 10 |
},
|
|
|
|
| 53 |
"DEBIAN_MIN_VERSION": "12",
|
| 54 |
"RHEL_MIN_VERSION": "9",
|
| 55 |
"PERSONAL_DATA_SCAN": "0",
|
| 56 |
+
"APPROVED_AI_TOOLS": "",
|
| 57 |
+
"IOS_MIN_MAJOR": "18",
|
| 58 |
+
"ANDROID_MIN_MAJOR": "13",
|
| 59 |
+
"MOBILE_PATCH_MAX_AGE_DAYS": "90",
|
| 60 |
+
"MOBILE_PASSCODE_MIN_DIGITS": "6"
|
| 61 |
},
|
| 62 |
"results": [
|
| 63 |
{
|
|
|
|
| 294 |
"section": "baseline",
|
| 295 |
"name": "Pending security updates",
|
| 296 |
"status": "Non-Compliant",
|
| 297 |
+
"actual": "15 pending",
|
| 298 |
"expected": "<= 0 pending",
|
| 299 |
+
"detail": "Including: curl, openssh-server, libsystemd0. Package lists are 1 days old."
|
| 300 |
},
|
| 301 |
{
|
| 302 |
"id": "vuln.outdated_apps",
|
| 303 |
"section": "baseline",
|
| 304 |
"name": "Outdated third-party applications",
|
| 305 |
+
"status": "Compliant",
|
| 306 |
+
"actual": "0 outdated",
|
| 307 |
"expected": "<= 0 outdated",
|
| 308 |
+
"detail": "packages: 0"
|
| 309 |
},
|
| 310 |
{
|
| 311 |
"id": "vuln.known_vulnerabilities",
|
|
|
|
| 314 |
"status": "Compliant",
|
| 315 |
"actual": "0 critical/high",
|
| 316 |
"expected": "<= 0 critical/high",
|
| 317 |
+
"detail": "Matched against offline OSV/NVD data from 2026-10-08. 609 source packages checked against ubuntu:22.04 advisories. 0 medium/low."
|
| 318 |
},
|
| 319 |
{
|
| 320 |
"id": "gdpr.auto_updates",
|
|
|
|
| 329 |
"section": "gdpr",
|
| 330 |
"name": "Security updates installed recently",
|
| 331 |
"status": "Compliant",
|
| 332 |
+
"actual": "6 days ago",
|
| 333 |
"expected": "<= 30 days ago",
|
| 334 |
+
"detail": "Last package upgrade: 2026-09-29 05:06:17"
|
| 335 |
},
|
| 336 |
{
|
| 337 |
"id": "gdpr.os_supported",
|
|
|
|
| 362 |
"section": "gdpr",
|
| 363 |
"name": "Backup configured",
|
| 364 |
"status": "Compliant",
|
| 365 |
+
"actual": "Timeshift",
|
| 366 |
"expected": "backup configured"
|
| 367 |
},
|
| 368 |
{
|
| 369 |
"id": "gdpr.backup_recent",
|
| 370 |
"section": "gdpr",
|
| 371 |
"name": "Recent backup",
|
| 372 |
+
"status": "Compliant",
|
| 373 |
+
"actual": "7 days ago",
|
| 374 |
+
"expected": "<= 7 days ago"
|
|
|
|
| 375 |
},
|
| 376 |
{
|
| 377 |
"id": "gdpr.removable_encryption",
|
|
|
|
| 419 |
"section": "aiact",
|
| 420 |
"name": "AI software inventory",
|
| 421 |
"status": "Compliant",
|
| 422 |
+
"actual": "no AI tools found",
|
| 423 |
+
"expected": "inventory taken"
|
|
|
|
| 424 |
},
|
| 425 |
{
|
| 426 |
"id": "aiact.unapproved",
|
| 427 |
"section": "aiact",
|
| 428 |
"name": "Unapproved AI tools (shadow AI)",
|
| 429 |
+
"status": "Compliant",
|
| 430 |
+
"actual": "none",
|
| 431 |
"expected": "only: none"
|
| 432 |
},
|
| 433 |
{
|
| 434 |
"id": "aiact.risk_classification",
|
| 435 |
"section": "aiact",
|
| 436 |
"name": "AI system risk classification",
|
| 437 |
+
"status": "NotApplicable",
|
| 438 |
+
"actual": "no AI tools found",
|
| 439 |
+
"expected": "every AI tool classified"
|
|
|
|
| 440 |
},
|
| 441 |
{
|
| 442 |
"id": "aiact.os_ai_features",
|
|
|
|
| 451 |
"id": "aiact.log_retention",
|
| 452 |
"section": "aiact",
|
| 453 |
"name": "AI usage log retention",
|
| 454 |
+
"status": "NotApplicable",
|
| 455 |
+
"actual": "no AI tools found",
|
| 456 |
+
"expected": ">= 6 months for high-risk AI"
|
|
|
|
| 457 |
},
|
| 458 |
{
|
| 459 |
"id": "org.ropa",
|
|
|
|
| 491 |
"id": "org.ai_register",
|
| 492 |
"section": "org",
|
| 493 |
"name": "AI register",
|
| 494 |
+
"status": "Non-Compliant",
|
| 495 |
+
"actual": "partial",
|
| 496 |
"expected": "yes"
|
| 497 |
},
|
| 498 |
{
|
|
|
|
| 515 |
"id": "org.ai_transparency",
|
| 516 |
"section": "org",
|
| 517 |
"name": "AI transparency to users",
|
| 518 |
+
"status": "Compliant",
|
| 519 |
+
"actual": "yes",
|
| 520 |
"expected": "yes"
|
| 521 |
}
|
| 522 |
],
|
| 523 |
"summary": {
|
| 524 |
+
"Compliant": 41,
|
| 525 |
+
"Non-Compliant": 11,
|
| 526 |
+
"NotApplicable": 4,
|
| 527 |
+
"Pending": 0,
|
| 528 |
"Error": 0
|
| 529 |
}
|
| 530 |
}
|
synthetic/samples/synthetic-macos-42000126.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
{
|
| 2 |
"schema_version": "1.0",
|
| 3 |
-
"catalog_version": "0.
|
| 4 |
"source": {
|
| 5 |
"kind": "synthetic",
|
| 6 |
"collector": "macos",
|
| 7 |
-
"collector_version": "0.
|
| 8 |
"synthetic_seed": 42000126,
|
| 9 |
"synthetic_profile": "hardened"
|
| 10 |
},
|
|
@@ -53,7 +53,11 @@
|
|
| 53 |
"DEBIAN_MIN_VERSION": "12",
|
| 54 |
"RHEL_MIN_VERSION": "9",
|
| 55 |
"PERSONAL_DATA_SCAN": "0",
|
| 56 |
-
"APPROVED_AI_TOOLS": ""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
},
|
| 58 |
"results": [
|
| 59 |
{
|
|
@@ -292,17 +296,18 @@
|
|
| 292 |
"section": "baseline",
|
| 293 |
"name": "Pending security updates",
|
| 294 |
"status": "Non-Compliant",
|
| 295 |
-
"actual": "
|
| 296 |
"expected": "<= 0 pending",
|
| 297 |
-
"detail": "Including:
|
| 298 |
},
|
| 299 |
{
|
| 300 |
"id": "vuln.outdated_apps",
|
| 301 |
"section": "baseline",
|
| 302 |
"name": "Outdated third-party applications",
|
| 303 |
-
"status": "
|
| 304 |
-
"actual": "
|
| 305 |
-
"expected": "<= 0 outdated"
|
|
|
|
| 306 |
},
|
| 307 |
{
|
| 308 |
"id": "vuln.known_vulnerabilities",
|
|
@@ -311,7 +316,7 @@
|
|
| 311 |
"status": "Compliant",
|
| 312 |
"actual": "0 critical/high",
|
| 313 |
"expected": "<= 0 critical/high",
|
| 314 |
-
"detail": "Matched against offline OSV/NVD data from 2026-10-08.
|
| 315 |
},
|
| 316 |
{
|
| 317 |
"id": "gdpr.auto_updates",
|
|
@@ -326,9 +331,9 @@
|
|
| 326 |
"section": "gdpr",
|
| 327 |
"name": "Security updates installed recently",
|
| 328 |
"status": "Compliant",
|
| 329 |
-
"actual": "
|
| 330 |
"expected": "<= 30 days ago",
|
| 331 |
-
"detail": "Last software update: 2026-09-
|
| 332 |
},
|
| 333 |
{
|
| 334 |
"id": "gdpr.os_supported",
|
|
@@ -360,17 +365,16 @@
|
|
| 360 |
"section": "gdpr",
|
| 361 |
"name": "Backup configured",
|
| 362 |
"status": "Compliant",
|
| 363 |
-
"actual": "
|
| 364 |
"expected": "backup configured"
|
| 365 |
},
|
| 366 |
{
|
| 367 |
"id": "gdpr.backup_recent",
|
| 368 |
"section": "gdpr",
|
| 369 |
"name": "Recent backup",
|
| 370 |
-
"status": "
|
| 371 |
-
"actual": "
|
| 372 |
-
"expected": "<= 7 days ago"
|
| 373 |
-
"detail": "Confirm the last backup date in bzserv."
|
| 374 |
},
|
| 375 |
{
|
| 376 |
"id": "gdpr.removable_encryption",
|
|
@@ -400,8 +404,8 @@
|
|
| 400 |
"id": "gdpr.telemetry",
|
| 401 |
"section": "gdpr",
|
| 402 |
"name": "Diagnostic data minimised",
|
| 403 |
-
"status": "
|
| 404 |
-
"actual": "
|
| 405 |
"expected": "analytics sharing off"
|
| 406 |
},
|
| 407 |
{
|
|
@@ -418,25 +422,24 @@
|
|
| 418 |
"section": "aiact",
|
| 419 |
"name": "AI software inventory",
|
| 420 |
"status": "Compliant",
|
| 421 |
-
"actual": "
|
| 422 |
"expected": "inventory taken"
|
| 423 |
},
|
| 424 |
{
|
| 425 |
"id": "aiact.unapproved",
|
| 426 |
"section": "aiact",
|
| 427 |
"name": "Unapproved AI tools (shadow AI)",
|
| 428 |
-
"status": "
|
| 429 |
-
"actual": "
|
| 430 |
"expected": "only: none"
|
| 431 |
},
|
| 432 |
{
|
| 433 |
"id": "aiact.risk_classification",
|
| 434 |
"section": "aiact",
|
| 435 |
"name": "AI system risk classification",
|
| 436 |
-
"status": "
|
| 437 |
-
"actual": "
|
| 438 |
-
"expected": "every AI tool classified"
|
| 439 |
-
"detail": "Classified by the auditor model (Caveat classifier) in a later step."
|
| 440 |
},
|
| 441 |
{
|
| 442 |
"id": "aiact.os_ai_features",
|
|
@@ -450,10 +453,9 @@
|
|
| 450 |
"id": "aiact.log_retention",
|
| 451 |
"section": "aiact",
|
| 452 |
"name": "AI usage log retention",
|
| 453 |
-
"status": "
|
| 454 |
-
"actual": "
|
| 455 |
-
"expected": ">= 6 months for high-risk AI"
|
| 456 |
-
"detail": "Required only if a tool is classified high-risk."
|
| 457 |
},
|
| 458 |
{
|
| 459 |
"id": "org.ropa",
|
|
@@ -475,8 +477,8 @@
|
|
| 475 |
"id": "org.breach_procedure",
|
| 476 |
"section": "org",
|
| 477 |
"name": "Breach response procedure",
|
| 478 |
-
"status": "
|
| 479 |
-
"actual": "
|
| 480 |
"expected": "yes"
|
| 481 |
},
|
| 482 |
{
|
|
@@ -521,10 +523,10 @@
|
|
| 521 |
}
|
| 522 |
],
|
| 523 |
"summary": {
|
| 524 |
-
"Compliant":
|
| 525 |
-
"Non-Compliant":
|
| 526 |
-
"NotApplicable":
|
| 527 |
-
"Pending":
|
| 528 |
"Error": 0
|
| 529 |
}
|
| 530 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"schema_version": "1.0",
|
| 3 |
+
"catalog_version": "0.3.0",
|
| 4 |
"source": {
|
| 5 |
"kind": "synthetic",
|
| 6 |
"collector": "macos",
|
| 7 |
+
"collector_version": "0.3.0",
|
| 8 |
"synthetic_seed": 42000126,
|
| 9 |
"synthetic_profile": "hardened"
|
| 10 |
},
|
|
|
|
| 53 |
"DEBIAN_MIN_VERSION": "12",
|
| 54 |
"RHEL_MIN_VERSION": "9",
|
| 55 |
"PERSONAL_DATA_SCAN": "0",
|
| 56 |
+
"APPROVED_AI_TOOLS": "",
|
| 57 |
+
"IOS_MIN_MAJOR": "18",
|
| 58 |
+
"ANDROID_MIN_MAJOR": "13",
|
| 59 |
+
"MOBILE_PATCH_MAX_AGE_DAYS": "90",
|
| 60 |
+
"MOBILE_PASSCODE_MIN_DIGITS": "6"
|
| 61 |
},
|
| 62 |
"results": [
|
| 63 |
{
|
|
|
|
| 296 |
"section": "baseline",
|
| 297 |
"name": "Pending security updates",
|
| 298 |
"status": "Non-Compliant",
|
| 299 |
+
"actual": "1 pending",
|
| 300 |
"expected": "<= 0 pending",
|
| 301 |
+
"detail": "Including: macOS Tahoe 26.1.2-22H250."
|
| 302 |
},
|
| 303 |
{
|
| 304 |
"id": "vuln.outdated_apps",
|
| 305 |
"section": "baseline",
|
| 306 |
"name": "Outdated third-party applications",
|
| 307 |
+
"status": "NotApplicable",
|
| 308 |
+
"actual": "not checked",
|
| 309 |
+
"expected": "<= 0 outdated",
|
| 310 |
+
"detail": "Homebrew is not installed, so there is no local source of current application versions."
|
| 311 |
},
|
| 312 |
{
|
| 313 |
"id": "vuln.known_vulnerabilities",
|
|
|
|
| 316 |
"status": "Compliant",
|
| 317 |
"actual": "0 critical/high",
|
| 318 |
"expected": "<= 0 critical/high",
|
| 319 |
+
"detail": "Matched against offline OSV/NVD data from 2026-10-08. 17 applications inventoried, 10 recognised (23 common applications covered). 0 medium/low."
|
| 320 |
},
|
| 321 |
{
|
| 322 |
"id": "gdpr.auto_updates",
|
|
|
|
| 331 |
"section": "gdpr",
|
| 332 |
"name": "Security updates installed recently",
|
| 333 |
"status": "Compliant",
|
| 334 |
+
"actual": "22 days ago",
|
| 335 |
"expected": "<= 30 days ago",
|
| 336 |
+
"detail": "Last software update: 2026-09-13 00:41:00 +0000"
|
| 337 |
},
|
| 338 |
{
|
| 339 |
"id": "gdpr.os_supported",
|
|
|
|
| 365 |
"section": "gdpr",
|
| 366 |
"name": "Backup configured",
|
| 367 |
"status": "Compliant",
|
| 368 |
+
"actual": "Time Machine",
|
| 369 |
"expected": "backup configured"
|
| 370 |
},
|
| 371 |
{
|
| 372 |
"id": "gdpr.backup_recent",
|
| 373 |
"section": "gdpr",
|
| 374 |
"name": "Recent backup",
|
| 375 |
+
"status": "Compliant",
|
| 376 |
+
"actual": "1 days ago",
|
| 377 |
+
"expected": "<= 7 days ago"
|
|
|
|
| 378 |
},
|
| 379 |
{
|
| 380 |
"id": "gdpr.removable_encryption",
|
|
|
|
| 404 |
"id": "gdpr.telemetry",
|
| 405 |
"section": "gdpr",
|
| 406 |
"name": "Diagnostic data minimised",
|
| 407 |
+
"status": "Compliant",
|
| 408 |
+
"actual": "analytics sharing off",
|
| 409 |
"expected": "analytics sharing off"
|
| 410 |
},
|
| 411 |
{
|
|
|
|
| 422 |
"section": "aiact",
|
| 423 |
"name": "AI software inventory",
|
| 424 |
"status": "Compliant",
|
| 425 |
+
"actual": "no AI tools found",
|
| 426 |
"expected": "inventory taken"
|
| 427 |
},
|
| 428 |
{
|
| 429 |
"id": "aiact.unapproved",
|
| 430 |
"section": "aiact",
|
| 431 |
"name": "Unapproved AI tools (shadow AI)",
|
| 432 |
+
"status": "Compliant",
|
| 433 |
+
"actual": "none",
|
| 434 |
"expected": "only: none"
|
| 435 |
},
|
| 436 |
{
|
| 437 |
"id": "aiact.risk_classification",
|
| 438 |
"section": "aiact",
|
| 439 |
"name": "AI system risk classification",
|
| 440 |
+
"status": "NotApplicable",
|
| 441 |
+
"actual": "no AI tools found",
|
| 442 |
+
"expected": "every AI tool classified"
|
|
|
|
| 443 |
},
|
| 444 |
{
|
| 445 |
"id": "aiact.os_ai_features",
|
|
|
|
| 453 |
"id": "aiact.log_retention",
|
| 454 |
"section": "aiact",
|
| 455 |
"name": "AI usage log retention",
|
| 456 |
+
"status": "NotApplicable",
|
| 457 |
+
"actual": "no AI tools found",
|
| 458 |
+
"expected": ">= 6 months for high-risk AI"
|
|
|
|
| 459 |
},
|
| 460 |
{
|
| 461 |
"id": "org.ropa",
|
|
|
|
| 477 |
"id": "org.breach_procedure",
|
| 478 |
"section": "org",
|
| 479 |
"name": "Breach response procedure",
|
| 480 |
+
"status": "Compliant",
|
| 481 |
+
"actual": "yes",
|
| 482 |
"expected": "yes"
|
| 483 |
},
|
| 484 |
{
|
|
|
|
| 523 |
}
|
| 524 |
],
|
| 525 |
"summary": {
|
| 526 |
+
"Compliant": 41,
|
| 527 |
+
"Non-Compliant": 9,
|
| 528 |
+
"NotApplicable": 6,
|
| 529 |
+
"Pending": 0,
|
| 530 |
"Error": 0
|
| 531 |
}
|
| 532 |
}
|
synthetic/samples/synthetic-windows-42000126.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
{
|
| 2 |
"schema_version": "1.0",
|
| 3 |
-
"catalog_version": "0.
|
| 4 |
"source": {
|
| 5 |
"kind": "synthetic",
|
| 6 |
"collector": "windows",
|
| 7 |
-
"collector_version": "0.
|
| 8 |
"synthetic_seed": 42000126,
|
| 9 |
"synthetic_profile": "hardened"
|
| 10 |
},
|
|
@@ -53,7 +53,11 @@
|
|
| 53 |
"DEBIAN_MIN_VERSION": "12",
|
| 54 |
"RHEL_MIN_VERSION": "9",
|
| 55 |
"PERSONAL_DATA_SCAN": "0",
|
| 56 |
-
"APPROVED_AI_TOOLS": ""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
},
|
| 58 |
"results": [
|
| 59 |
{
|
|
@@ -361,9 +365,10 @@
|
|
| 361 |
"id": "vuln.outdated_apps",
|
| 362 |
"section": "baseline",
|
| 363 |
"name": "Outdated third-party applications",
|
| 364 |
-
"status": "
|
| 365 |
-
"actual": "
|
| 366 |
-
"expected": "<= 0 outdated"
|
|
|
|
| 367 |
},
|
| 368 |
{
|
| 369 |
"id": "vuln.known_vulnerabilities",
|
|
@@ -372,14 +377,14 @@
|
|
| 372 |
"status": "Compliant",
|
| 373 |
"actual": "0 critical/high",
|
| 374 |
"expected": "<= 0 critical/high",
|
| 375 |
-
"detail": "Matched against offline OSV/NVD data from 2026-
|
| 376 |
},
|
| 377 |
{
|
| 378 |
"id": "gdpr.auto_updates",
|
| 379 |
"section": "gdpr",
|
| 380 |
"name": "Automatic security updates",
|
| 381 |
"status": "Compliant",
|
| 382 |
-
"actual": "
|
| 383 |
"expected": "automatic"
|
| 384 |
},
|
| 385 |
{
|
|
@@ -387,9 +392,9 @@
|
|
| 387 |
"section": "gdpr",
|
| 388 |
"name": "Security updates installed recently",
|
| 389 |
"status": "Compliant",
|
| 390 |
-
"actual": "
|
| 391 |
"expected": "<= 30 days ago",
|
| 392 |
-
"detail": "Last update: 2026-
|
| 393 |
},
|
| 394 |
{
|
| 395 |
"id": "gdpr.os_supported",
|
|
@@ -412,7 +417,7 @@
|
|
| 412 |
"section": "gdpr",
|
| 413 |
"name": "Time synchronisation",
|
| 414 |
"status": "Compliant",
|
| 415 |
-
"actual": "type
|
| 416 |
"expected": "NTP or domain time"
|
| 417 |
},
|
| 418 |
{
|
|
@@ -452,8 +457,8 @@
|
|
| 452 |
"id": "gdpr.remote_access",
|
| 453 |
"section": "gdpr",
|
| 454 |
"name": "Remote access services restricted",
|
| 455 |
-
"status": "
|
| 456 |
-
"actual": "
|
| 457 |
"expected": "only: none"
|
| 458 |
},
|
| 459 |
{
|
|
@@ -461,7 +466,7 @@
|
|
| 461 |
"section": "gdpr",
|
| 462 |
"name": "Diagnostic data minimised",
|
| 463 |
"status": "Compliant",
|
| 464 |
-
"actual": "AllowTelemetry=
|
| 465 |
"expected": "required diagnostic data only (0-1)"
|
| 466 |
},
|
| 467 |
{
|
|
@@ -478,16 +483,15 @@
|
|
| 478 |
"section": "aiact",
|
| 479 |
"name": "AI software inventory",
|
| 480 |
"status": "Compliant",
|
| 481 |
-
"actual": "
|
| 482 |
-
"expected": "inventory taken"
|
| 483 |
-
"detail": "Local AI service listening on port 11434. Local models: C:\\Users\\jdoe\\.ollama\\models (51 GB)."
|
| 484 |
},
|
| 485 |
{
|
| 486 |
"id": "aiact.unapproved",
|
| 487 |
"section": "aiact",
|
| 488 |
"name": "Unapproved AI tools (shadow AI)",
|
| 489 |
"status": "Non-Compliant",
|
| 490 |
-
"actual": "
|
| 491 |
"expected": "only: none"
|
| 492 |
},
|
| 493 |
{
|
|
@@ -495,7 +499,7 @@
|
|
| 495 |
"section": "aiact",
|
| 496 |
"name": "AI system risk classification",
|
| 497 |
"status": "Pending",
|
| 498 |
-
"actual": "
|
| 499 |
"expected": "every AI tool classified",
|
| 500 |
"detail": "Classified by the auditor model (Caveat classifier) in a later step."
|
| 501 |
},
|
|
@@ -503,10 +507,10 @@
|
|
| 503 |
"id": "aiact.os_ai_features",
|
| 504 |
"section": "aiact",
|
| 505 |
"name": "Windows Recall / Copilot data analysis disabled",
|
| 506 |
-
"status": "
|
| 507 |
-
"actual": "Recall
|
| 508 |
"expected": "disabled by policy",
|
| 509 |
-
"detail": "DisableAIDataAnalysis=; AllowRecallEnablement=; TurnOffWindowsCopilot="
|
| 510 |
},
|
| 511 |
{
|
| 512 |
"id": "aiact.log_retention",
|
|
@@ -521,8 +525,8 @@
|
|
| 521 |
"id": "org.ropa",
|
| 522 |
"section": "org",
|
| 523 |
"name": "Record of processing activities",
|
| 524 |
-
"status": "Compliant",
|
| 525 |
-
"actual": "
|
| 526 |
"expected": "yes"
|
| 527 |
},
|
| 528 |
{
|
|
@@ -577,17 +581,16 @@
|
|
| 577 |
"id": "org.ai_transparency",
|
| 578 |
"section": "org",
|
| 579 |
"name": "AI transparency to users",
|
| 580 |
-
"status": "
|
| 581 |
-
"actual": "
|
| 582 |
-
"expected": "yes"
|
| 583 |
-
"detail": "Answer ORG_AI_TRANSPARENCY in organisational.conf."
|
| 584 |
}
|
| 585 |
],
|
| 586 |
"summary": {
|
| 587 |
-
"Compliant":
|
| 588 |
"Non-Compliant": 3,
|
| 589 |
"NotApplicable": 2,
|
| 590 |
-
"Pending":
|
| 591 |
"Error": 0
|
| 592 |
}
|
| 593 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"schema_version": "1.0",
|
| 3 |
+
"catalog_version": "0.3.0",
|
| 4 |
"source": {
|
| 5 |
"kind": "synthetic",
|
| 6 |
"collector": "windows",
|
| 7 |
+
"collector_version": "0.3.0",
|
| 8 |
"synthetic_seed": 42000126,
|
| 9 |
"synthetic_profile": "hardened"
|
| 10 |
},
|
|
|
|
| 53 |
"DEBIAN_MIN_VERSION": "12",
|
| 54 |
"RHEL_MIN_VERSION": "9",
|
| 55 |
"PERSONAL_DATA_SCAN": "0",
|
| 56 |
+
"APPROVED_AI_TOOLS": "",
|
| 57 |
+
"IOS_MIN_MAJOR": "18",
|
| 58 |
+
"ANDROID_MIN_MAJOR": "13",
|
| 59 |
+
"MOBILE_PATCH_MAX_AGE_DAYS": "90",
|
| 60 |
+
"MOBILE_PASSCODE_MIN_DIGITS": "6"
|
| 61 |
},
|
| 62 |
"results": [
|
| 63 |
{
|
|
|
|
| 365 |
"id": "vuln.outdated_apps",
|
| 366 |
"section": "baseline",
|
| 367 |
"name": "Outdated third-party applications",
|
| 368 |
+
"status": "NotApplicable",
|
| 369 |
+
"actual": "not checked",
|
| 370 |
+
"expected": "<= 0 outdated",
|
| 371 |
+
"detail": "winget (App Installer) is not available for this account, so there is no local source of current application versions."
|
| 372 |
},
|
| 373 |
{
|
| 374 |
"id": "vuln.known_vulnerabilities",
|
|
|
|
| 377 |
"status": "Compliant",
|
| 378 |
"actual": "0 critical/high",
|
| 379 |
"expected": "<= 0 critical/high",
|
| 380 |
+
"detail": "Matched against offline OSV/NVD data from 2026-10-08. 14 applications inventoried, 11 recognised (31 common applications covered). 0 medium/low."
|
| 381 |
},
|
| 382 |
{
|
| 383 |
"id": "gdpr.auto_updates",
|
| 384 |
"section": "gdpr",
|
| 385 |
"name": "Automatic security updates",
|
| 386 |
"status": "Compliant",
|
| 387 |
+
"actual": "Windows default (automatic)",
|
| 388 |
"expected": "automatic"
|
| 389 |
},
|
| 390 |
{
|
|
|
|
| 392 |
"section": "gdpr",
|
| 393 |
"name": "Security updates installed recently",
|
| 394 |
"status": "Compliant",
|
| 395 |
+
"actual": "18 days ago",
|
| 396 |
"expected": "<= 30 days ago",
|
| 397 |
+
"detail": "Last update: 2026-07 Cumulative Update for Windows 11 (2026-09-17)"
|
| 398 |
},
|
| 399 |
{
|
| 400 |
"id": "gdpr.os_supported",
|
|
|
|
| 417 |
"section": "gdpr",
|
| 418 |
"name": "Time synchronisation",
|
| 419 |
"status": "Compliant",
|
| 420 |
+
"actual": "type NT5DS, service Running/Manual",
|
| 421 |
"expected": "NTP or domain time"
|
| 422 |
},
|
| 423 |
{
|
|
|
|
| 457 |
"id": "gdpr.remote_access",
|
| 458 |
"section": "gdpr",
|
| 459 |
"name": "Remote access services restricted",
|
| 460 |
+
"status": "Compliant",
|
| 461 |
+
"actual": "none enabled",
|
| 462 |
"expected": "only: none"
|
| 463 |
},
|
| 464 |
{
|
|
|
|
| 466 |
"section": "gdpr",
|
| 467 |
"name": "Diagnostic data minimised",
|
| 468 |
"status": "Compliant",
|
| 469 |
+
"actual": "AllowTelemetry=1 (Settings)",
|
| 470 |
"expected": "required diagnostic data only (0-1)"
|
| 471 |
},
|
| 472 |
{
|
|
|
|
| 483 |
"section": "aiact",
|
| 484 |
"name": "AI software inventory",
|
| 485 |
"status": "Compliant",
|
| 486 |
+
"actual": "Claude Code",
|
| 487 |
+
"expected": "inventory taken"
|
|
|
|
| 488 |
},
|
| 489 |
{
|
| 490 |
"id": "aiact.unapproved",
|
| 491 |
"section": "aiact",
|
| 492 |
"name": "Unapproved AI tools (shadow AI)",
|
| 493 |
"status": "Non-Compliant",
|
| 494 |
+
"actual": "Claude Code",
|
| 495 |
"expected": "only: none"
|
| 496 |
},
|
| 497 |
{
|
|
|
|
| 499 |
"section": "aiact",
|
| 500 |
"name": "AI system risk classification",
|
| 501 |
"status": "Pending",
|
| 502 |
+
"actual": "Claude Code",
|
| 503 |
"expected": "every AI tool classified",
|
| 504 |
"detail": "Classified by the auditor model (Caveat classifier) in a later step."
|
| 505 |
},
|
|
|
|
| 507 |
"id": "aiact.os_ai_features",
|
| 508 |
"section": "aiact",
|
| 509 |
"name": "Windows Recall / Copilot data analysis disabled",
|
| 510 |
+
"status": "Compliant",
|
| 511 |
+
"actual": "Recall disabled by policy",
|
| 512 |
"expected": "disabled by policy",
|
| 513 |
+
"detail": "DisableAIDataAnalysis=1; AllowRecallEnablement=0; TurnOffWindowsCopilot="
|
| 514 |
},
|
| 515 |
{
|
| 516 |
"id": "aiact.log_retention",
|
|
|
|
| 525 |
"id": "org.ropa",
|
| 526 |
"section": "org",
|
| 527 |
"name": "Record of processing activities",
|
| 528 |
+
"status": "Non-Compliant",
|
| 529 |
+
"actual": "partial",
|
| 530 |
"expected": "yes"
|
| 531 |
},
|
| 532 |
{
|
|
|
|
| 581 |
"id": "org.ai_transparency",
|
| 582 |
"section": "org",
|
| 583 |
"name": "AI transparency to users",
|
| 584 |
+
"status": "Compliant",
|
| 585 |
+
"actual": "yes",
|
| 586 |
+
"expected": "yes"
|
|
|
|
| 587 |
}
|
| 588 |
],
|
| 589 |
"summary": {
|
| 590 |
+
"Compliant": 56,
|
| 591 |
"Non-Compliant": 3,
|
| 592 |
"NotApplicable": 2,
|
| 593 |
+
"Pending": 3,
|
| 594 |
"Error": 0
|
| 595 |
}
|
| 596 |
}
|
synthetic/vuln-helper.mjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Synthetic data helper: run generated software inventories through the real matcher (app/renderer/vuln-core.js)
|
| 2 |
+
// and the real offline bundle, so training data has real CVEs in exactly the app's wording.
|
| 3 |
+
// stdin: JSON lines {os_family, inventory, max, now} -> stdout: JSON lines {status, actual, detail}
|
| 4 |
+
import { readFileSync } from 'node:fs';
|
| 5 |
+
import { gunzipSync } from 'node:zlib';
|
| 6 |
+
import path from 'node:path';
|
| 7 |
+
import { fileURLToPath } from 'node:url';
|
| 8 |
+
import { matchLinux, matchApps, knownVulnResult } from '../app/renderer/vuln-core.js';
|
| 9 |
+
|
| 10 |
+
const dir = process.argv[2] || path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'vulndb', 'out');
|
| 11 |
+
const manifest = JSON.parse(readFileSync(path.join(dir, 'manifest.json'), 'utf8'));
|
| 12 |
+
const bundle = JSON.parse(gunzipSync(readFileSync(path.join(dir, manifest.file))).toString('utf8'));
|
| 13 |
+
for (const line of readFileSync(0, 'utf8').split('\n').filter(Boolean)) {
|
| 14 |
+
const job = JSON.parse(line);
|
| 15 |
+
const matched = job.os_family === 'linux' ? matchLinux(job.inventory, bundle) : matchApps(job.inventory, bundle, job.os_family);
|
| 16 |
+
process.stdout.write(JSON.stringify(knownVulnResult(matched, job.inventory, { bundle, manifest }, job.os_family, job.max, Date.parse(job.now))) + '\n');
|
| 17 |
+
}
|
tools/build_controls_doc.py
CHANGED
|
@@ -73,11 +73,37 @@ SOURCES = {
|
|
| 73 |
"aiact.log_retention": {"windows": "depends on AI classification", "linux": "depends on AI classification", "macos": "depends on AI classification"},
|
| 74 |
}
|
| 75 |
ORG_SOURCE = "organisational.conf (answered once by the organisation)"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
|
| 78 |
def main():
|
| 79 |
with open(os.path.join(ROOT, "catalog", "checks.json"), encoding="utf-8") as handle:
|
| 80 |
catalog = json.load(handle)
|
|
|
|
|
|
|
| 81 |
titles = catalog["references"]
|
| 82 |
lines = [
|
| 83 |
"# Audited controls and data sources",
|
|
@@ -85,26 +111,30 @@ def main():
|
|
| 85 |
f"Catalog version {catalog['catalog_version']} · {len(catalog['checks'])} checks. Generated by `tools/build_controls_doc.py` — do not edit by hand.",
|
| 86 |
"",
|
| 87 |
"All sources are read-only. Internet is only used by *Pending security updates* on Windows and macOS, and by the app to refresh the vulnerability bundle.",
|
|
|
|
| 88 |
"",
|
| 89 |
]
|
| 90 |
for section in catalog["sections"]:
|
| 91 |
checks = [c for c in catalog["checks"] if c["section"] == section["id"]]
|
| 92 |
lines += [f"## {section['title']} ({len(checks)})", "",
|
| 93 |
-
"| Check | Applies to | References | Rule | Windows source | Linux source | macOS source |",
|
| 94 |
-
"|---|---|---|---|---|---|---|"]
|
| 95 |
for c in checks:
|
| 96 |
refs = [f"ISO {r}" for r in c["controls"]["iso27001"]] + [f"GDPR {r}" for r in c["controls"]["gdpr"]] + [f"AI Act {r}" for r in c["controls"]["ai_act"]]
|
| 97 |
src = SOURCES.get(c["id"], {})
|
| 98 |
cells = []
|
| 99 |
-
for os_family in ("windows", "linux", "macos"):
|
| 100 |
if os_family not in c["os"]:
|
| 101 |
cells.append("—")
|
| 102 |
elif section["id"] == "org":
|
| 103 |
-
cells.append(ORG_SOURCE)
|
|
|
|
|
|
|
| 104 |
else:
|
| 105 |
cells.append(src.get(os_family, "**missing**"))
|
| 106 |
names = c.get("names", {})
|
| 107 |
-
|
|
|
|
| 108 |
lines.append(f"| {name} | {', '.join(c['os'])} | {'; '.join(refs) or '—'} | {c['rule']} | " + " | ".join(cells) + " |")
|
| 109 |
lines.append("")
|
| 110 |
lines += ["## Reference titles", ""]
|
|
@@ -115,7 +145,8 @@ def main():
|
|
| 115 |
out = os.path.join(ROOT, "docs", "CONTROLS.md")
|
| 116 |
with open(out, "w", encoding="utf-8") as handle:
|
| 117 |
handle.write("\n".join(lines))
|
| 118 |
-
missing = [c[
|
|
|
|
| 119 |
print(f"Wrote {out}; checks without a documented source: {missing or 'none'}")
|
| 120 |
|
| 121 |
|
|
|
|
| 73 |
"aiact.log_retention": {"windows": "depends on AI classification", "linux": "depends on AI classification", "macos": "depends on AI classification"},
|
| 74 |
}
|
| 75 |
ORG_SOURCE = "organisational.conf (answered once by the organisation)"
|
| 76 |
+
# Phones: what the browser detects; everything else comes from catalog/mobile_questions.json (asked, self-reported).
|
| 77 |
+
PHONE_DETECTED = {
|
| 78 |
+
"gdpr.os_supported": "detected: OS version (Safari version on iOS 26+, client hints on Android), confirmed by the user",
|
| 79 |
+
"vuln.known_vulnerabilities": "detected: iOS version matched against NVD iOS/iPadOS records in the signed phone bundle",
|
| 80 |
+
"encryption.enabled": {"ios": "derived: on when a passcode is set (iOS data protection)", "android": "detected: Android 10+ encrypts by default"},
|
| 81 |
+
"screen_lock.enabled": "detected: WebAuthn platform authenticator available; otherwise asked",
|
| 82 |
+
}
|
| 83 |
+
PHONE_AI = "asked: AI apps installed (checklist); classified by the auditor model"
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def phone_source(check_id, os_family, questions):
|
| 87 |
+
det = PHONE_DETECTED.get(check_id)
|
| 88 |
+
if isinstance(det, dict):
|
| 89 |
+
det = det.get(os_family)
|
| 90 |
+
if check_id == "vuln.pending_updates" and os_family == "ios":
|
| 91 |
+
return "detected: iOS version vs newest fixed release in the signed phone bundle"
|
| 92 |
+
if det and check_id != "screen_lock.enabled":
|
| 93 |
+
return det
|
| 94 |
+
spec = questions["checks"].get(check_id, {}).get(os_family)
|
| 95 |
+
if spec:
|
| 96 |
+
return (det + "; " if det else "") + f"asked: {spec['where']}"
|
| 97 |
+
if check_id.startswith("aiact."):
|
| 98 |
+
return PHONE_AI
|
| 99 |
+
return None
|
| 100 |
|
| 101 |
|
| 102 |
def main():
|
| 103 |
with open(os.path.join(ROOT, "catalog", "checks.json"), encoding="utf-8") as handle:
|
| 104 |
catalog = json.load(handle)
|
| 105 |
+
with open(os.path.join(ROOT, "catalog", "mobile_questions.json"), encoding="utf-8") as handle:
|
| 106 |
+
questions = json.load(handle)
|
| 107 |
titles = catalog["references"]
|
| 108 |
lines = [
|
| 109 |
"# Audited controls and data sources",
|
|
|
|
| 111 |
f"Catalog version {catalog['catalog_version']} · {len(catalog['checks'])} checks. Generated by `tools/build_controls_doc.py` — do not edit by hand.",
|
| 112 |
"",
|
| 113 |
"All sources are read-only. Internet is only used by *Pending security updates* on Windows and macOS, and by the app to refresh the vulnerability bundle.",
|
| 114 |
+
"Phones are checked in the browser: a web page cannot read a phone's settings, so results are either detected by the browser or answered by the user (self-reported), and labelled as such.",
|
| 115 |
"",
|
| 116 |
]
|
| 117 |
for section in catalog["sections"]:
|
| 118 |
checks = [c for c in catalog["checks"] if c["section"] == section["id"]]
|
| 119 |
lines += [f"## {section['title']} ({len(checks)})", "",
|
| 120 |
+
"| Check | Applies to | References | Rule | Windows source | Linux source | macOS source | iPhone source | Android source |",
|
| 121 |
+
"|---|---|---|---|---|---|---|---|---|"]
|
| 122 |
for c in checks:
|
| 123 |
refs = [f"ISO {r}" for r in c["controls"]["iso27001"]] + [f"GDPR {r}" for r in c["controls"]["gdpr"]] + [f"AI Act {r}" for r in c["controls"]["ai_act"]]
|
| 124 |
src = SOURCES.get(c["id"], {})
|
| 125 |
cells = []
|
| 126 |
+
for os_family in ("windows", "linux", "macos", "ios", "android"):
|
| 127 |
if os_family not in c["os"]:
|
| 128 |
cells.append("—")
|
| 129 |
elif section["id"] == "org":
|
| 130 |
+
cells.append(ORG_SOURCE if os_family not in ("ios", "android") else ORG_SOURCE + "; Pending in a phone check unless passed in the link")
|
| 131 |
+
elif os_family in ("ios", "android"):
|
| 132 |
+
cells.append(phone_source(c["id"], os_family, questions) or "**missing**")
|
| 133 |
else:
|
| 134 |
cells.append(src.get(os_family, "**missing**"))
|
| 135 |
names = c.get("names", {})
|
| 136 |
+
others = sorted(set(names.values()) - {c["name"]})
|
| 137 |
+
name = c["name"] + (" / " + " / ".join(others) if others else "")
|
| 138 |
lines.append(f"| {name} | {', '.join(c['os'])} | {'; '.join(refs) or '—'} | {c['rule']} | " + " | ".join(cells) + " |")
|
| 139 |
lines.append("")
|
| 140 |
lines += ["## Reference titles", ""]
|
|
|
|
| 145 |
out = os.path.join(ROOT, "docs", "CONTROLS.md")
|
| 146 |
with open(out, "w", encoding="utf-8") as handle:
|
| 147 |
handle.write("\n".join(lines))
|
| 148 |
+
missing = [f"{c['id']} ({o})" for c in catalog["checks"] if c["section"] != "org" for o in c["os"]
|
| 149 |
+
if (o in ("ios", "android") and not phone_source(c["id"], o, questions)) or (o not in ("ios", "android") and o not in SOURCES.get(c["id"], {}))]
|
| 150 |
print(f"Wrote {out}; checks without a documented source: {missing or 'none'}")
|
| 151 |
|
| 152 |
|
tools/validate_evidence.py
CHANGED
|
@@ -15,7 +15,7 @@ import sys
|
|
| 15 |
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 16 |
REQUIRED = {"schema_version", "catalog_version", "source", "audit", "host", "baseline", "results", "summary"}
|
| 17 |
RESULT_KEYS = {"id", "section", "name", "status", "actual", "expected"}
|
| 18 |
-
OPTIONAL_RESULT_KEYS = {"detail"}
|
| 19 |
|
| 20 |
|
| 21 |
def load_catalog():
|
|
@@ -38,10 +38,10 @@ def validate(record, catalog, checks):
|
|
| 38 |
errors.append(f"schema_version {record['schema_version']!r} != '1.0'")
|
| 39 |
if record["catalog_version"] != catalog["catalog_version"]:
|
| 40 |
errors.append(f"catalog_version {record['catalog_version']} != {catalog['catalog_version']}")
|
| 41 |
-
if record["source"].get("kind") not in ("collector", "synthetic"):
|
| 42 |
-
errors.append("source.kind must be collector or
|
| 43 |
os_family = record["host"].get("os_family")
|
| 44 |
-
if os_family not in ("windows", "linux", "macos"):
|
| 45 |
errors.append(f"host.os_family invalid: {os_family!r}")
|
| 46 |
return errors
|
| 47 |
if record["source"].get("collector") != os_family:
|
|
@@ -82,6 +82,12 @@ def validate(record, catalog, checks):
|
|
| 82 |
errors.append(f"{where}: {check_id} name {result['name']!r} != catalog {expected_name!r}")
|
| 83 |
if result["status"] not in catalog["statuses"]:
|
| 84 |
errors.append(f"{where}: {check_id} invalid status {result['status']!r}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
for key in ("actual", "expected") + (("detail",) if "detail" in result else ()):
|
| 86 |
if not isinstance(result[key], str):
|
| 87 |
errors.append(f"{where}: {check_id}.{key} must be a string")
|
|
|
|
| 15 |
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 16 |
REQUIRED = {"schema_version", "catalog_version", "source", "audit", "host", "baseline", "results", "summary"}
|
| 17 |
RESULT_KEYS = {"id", "section", "name", "status", "actual", "expected"}
|
| 18 |
+
OPTIONAL_RESULT_KEYS = {"detail", "evidence"}
|
| 19 |
|
| 20 |
|
| 21 |
def load_catalog():
|
|
|
|
| 38 |
errors.append(f"schema_version {record['schema_version']!r} != '1.0'")
|
| 39 |
if record["catalog_version"] != catalog["catalog_version"]:
|
| 40 |
errors.append(f"catalog_version {record['catalog_version']} != {catalog['catalog_version']}")
|
| 41 |
+
if record["source"].get("kind") not in ("collector", "synthetic", "web"):
|
| 42 |
+
errors.append("source.kind must be collector, synthetic or web")
|
| 43 |
os_family = record["host"].get("os_family")
|
| 44 |
+
if os_family not in ("windows", "linux", "macos", "ios", "android"):
|
| 45 |
errors.append(f"host.os_family invalid: {os_family!r}")
|
| 46 |
return errors
|
| 47 |
if record["source"].get("collector") != os_family:
|
|
|
|
| 82 |
errors.append(f"{where}: {check_id} name {result['name']!r} != catalog {expected_name!r}")
|
| 83 |
if result["status"] not in catalog["statuses"]:
|
| 84 |
errors.append(f"{where}: {check_id} invalid status {result['status']!r}")
|
| 85 |
+
if "evidence" in result and result["evidence"] not in ("detected", "self-reported"):
|
| 86 |
+
errors.append(f"{where}: {check_id} evidence must be detected or self-reported")
|
| 87 |
+
if os_family in ("ios", "android") and check["section"] != "org" and "evidence" not in result:
|
| 88 |
+
errors.append(f"{where}: {check_id} needs an evidence source on phones")
|
| 89 |
+
if os_family not in ("ios", "android") and "evidence" in result:
|
| 90 |
+
errors.append(f"{where}: {check_id} evidence source is only used on phones")
|
| 91 |
for key in ("actual", "expected") + (("detail",) if "detail" in result else ()):
|
| 92 |
if not isinstance(result[key], str):
|
| 93 |
errors.append(f"{where}: {check_id}.{key} must be a string")
|
training/build_dataset.py
CHANGED
|
@@ -24,7 +24,7 @@ import random
|
|
| 24 |
import re
|
| 25 |
|
| 26 |
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 27 |
-
DATASET_VERSION = "0.
|
| 28 |
SYSTEM_PROMPT = ("You are the Devseis endpoint auditor. You write audit findings from collected evidence for "
|
| 29 |
"ISO 27001, GDPR and the EU AI Act. Use only the evidence and reference material in the request. "
|
| 30 |
"Never invent values, dates, versions, account names or tools. Reply with JSON only.")
|
|
@@ -115,6 +115,16 @@ FINDING = {
|
|
| 115 |
}
|
| 116 |
|
| 117 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
def sentence(text):
|
| 119 |
text = text.strip()
|
| 120 |
return text if not text or text.endswith((".", "!", "?")) else text + "."
|
|
@@ -139,6 +149,8 @@ def build_finding(record, result, lib, rng):
|
|
| 139 |
]
|
| 140 |
if detail:
|
| 141 |
lines.append(f"DETAIL: {detail}")
|
|
|
|
|
|
|
| 142 |
lines.append("REFERENCES:")
|
| 143 |
lines += [f"- {label} {title}: {summary}" for label, title, summary in refs]
|
| 144 |
lines.append("GUIDANCE:")
|
|
@@ -158,6 +170,8 @@ def build_finding(record, result, lib, rng):
|
|
| 158 |
"detail": sentence(detail) if detail else "no detail", "reason": sentence(detail or result["actual"]),
|
| 159 |
"pending": sentence(detail) if detail else lib.status_notes["Pending"]}
|
| 160 |
finding = rng.choice(FINDING[status]).format(**fields)
|
|
|
|
|
|
|
| 161 |
references = [label for label, _, _ in refs]
|
| 162 |
|
| 163 |
if status == "Compliant":
|
|
@@ -185,6 +199,8 @@ def build_finding(record, result, lib, rng):
|
|
| 185 |
"finding": finding.strip(), "risk": risk, "recommendation": recommendation, "references": references}
|
| 186 |
if verification:
|
| 187 |
answer["verification"] = verification
|
|
|
|
|
|
|
| 188 |
return "\n".join(lines), answer
|
| 189 |
|
| 190 |
|
|
@@ -196,7 +212,11 @@ def build_summary(record, lib):
|
|
| 196 |
host = record["host"]
|
| 197 |
lines = ["TASK: summary", f"DEVICE: {device_line(record)}", f"HOSTNAME: {host['hostname']}",
|
| 198 |
f"AUDIT RUN AS ADMIN: {'yes' if record['audit']['is_admin'] else 'no'}",
|
| 199 |
-
"COUNTS: " + ", ".join(f"{k}={v}" for k, v in summary.items())
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
lines += [f"- {r['id']} | {r['name']} | {r['status']} | {r['actual']}" for r in results]
|
| 201 |
lines.append("RISK LEVELS (from guidance):")
|
| 202 |
gaps = [r for r in results if r["status"] == "Non-Compliant"]
|
|
@@ -227,6 +247,8 @@ def build_summary(record, lib):
|
|
| 227 |
text += f" {summary['Error']} checks could not be read" + (" because the audit was not run as administrator." if not record["audit"]["is_admin"] else ".")
|
| 228 |
if summary["Pending"]:
|
| 229 |
text += f" {summary['Pending']} items are pending."
|
|
|
|
|
|
|
| 230 |
|
| 231 |
next_steps = []
|
| 232 |
if summary["Error"]:
|
|
|
|
| 24 |
import re
|
| 25 |
|
| 26 |
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 27 |
+
DATASET_VERSION = "0.3.0"
|
| 28 |
SYSTEM_PROMPT = ("You are the Devseis endpoint auditor. You write audit findings from collected evidence for "
|
| 29 |
"ISO 27001, GDPR and the EU AI Act. Use only the evidence and reference material in the request. "
|
| 30 |
"Never invent values, dates, versions, account names or tools. Reply with JSON only.")
|
|
|
|
| 115 |
}
|
| 116 |
|
| 117 |
|
| 118 |
+
EVIDENCE_TEXT = {"self-reported": "self-reported by the user", "detected": "detected by the browser"}
|
| 119 |
+
SELF_REPORTED = "Self-reported by the user, not verified on the device."
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def evidence_counts(results):
|
| 123 |
+
detected = sum(1 for r in results if r.get("evidence") == "detected")
|
| 124 |
+
reported = sum(1 for r in results if r.get("evidence") == "self-reported")
|
| 125 |
+
return detected, reported
|
| 126 |
+
|
| 127 |
+
|
| 128 |
def sentence(text):
|
| 129 |
text = text.strip()
|
| 130 |
return text if not text or text.endswith((".", "!", "?")) else text + "."
|
|
|
|
| 149 |
]
|
| 150 |
if detail:
|
| 151 |
lines.append(f"DETAIL: {detail}")
|
| 152 |
+
if result.get("evidence"):
|
| 153 |
+
lines.append(f"EVIDENCE: {EVIDENCE_TEXT[result['evidence']]}")
|
| 154 |
lines.append("REFERENCES:")
|
| 155 |
lines += [f"- {label} {title}: {summary}" for label, title, summary in refs]
|
| 156 |
lines.append("GUIDANCE:")
|
|
|
|
| 170 |
"detail": sentence(detail) if detail else "no detail", "reason": sentence(detail or result["actual"]),
|
| 171 |
"pending": sentence(detail) if detail else lib.status_notes["Pending"]}
|
| 172 |
finding = rng.choice(FINDING[status]).format(**fields)
|
| 173 |
+
if result.get("evidence") == "self-reported":
|
| 174 |
+
finding = finding.replace("The device reports", "The user reports") + " " + SELF_REPORTED
|
| 175 |
references = [label for label, _, _ in refs]
|
| 176 |
|
| 177 |
if status == "Compliant":
|
|
|
|
| 199 |
"finding": finding.strip(), "risk": risk, "recommendation": recommendation, "references": references}
|
| 200 |
if verification:
|
| 201 |
answer["verification"] = verification
|
| 202 |
+
if result.get("evidence"):
|
| 203 |
+
answer["evidence"] = result["evidence"]
|
| 204 |
return "\n".join(lines), answer
|
| 205 |
|
| 206 |
|
|
|
|
| 212 |
host = record["host"]
|
| 213 |
lines = ["TASK: summary", f"DEVICE: {device_line(record)}", f"HOSTNAME: {host['hostname']}",
|
| 214 |
f"AUDIT RUN AS ADMIN: {'yes' if record['audit']['is_admin'] else 'no'}",
|
| 215 |
+
"COUNTS: " + ", ".join(f"{k}={v}" for k, v in summary.items())]
|
| 216 |
+
detected, reported = evidence_counts(results)
|
| 217 |
+
if detected or reported:
|
| 218 |
+
lines.append(f"EVIDENCE: {detected} detected by the browser, {reported} self-reported by the user")
|
| 219 |
+
lines.append("RESULTS (id | name | status | found):")
|
| 220 |
lines += [f"- {r['id']} | {r['name']} | {r['status']} | {r['actual']}" for r in results]
|
| 221 |
lines.append("RISK LEVELS (from guidance):")
|
| 222 |
gaps = [r for r in results if r["status"] == "Non-Compliant"]
|
|
|
|
| 247 |
text += f" {summary['Error']} checks could not be read" + (" because the audit was not run as administrator." if not record["audit"]["is_admin"] else ".")
|
| 248 |
if summary["Pending"]:
|
| 249 |
text += f" {summary['Pending']} items are pending."
|
| 250 |
+
if reported:
|
| 251 |
+
text += f" {reported} results are self-reported by the user and were not verified on the device."
|
| 252 |
|
| 253 |
next_steps = []
|
| 254 |
if summary["Error"]:
|
vulndb/build_bundle.py
CHANGED
|
@@ -215,7 +215,7 @@ def build_osv(stats):
|
|
| 215 |
# --- NVD (desktop applications) ----------------------------------------------------------------
|
| 216 |
def nvd_query(cpe):
|
| 217 |
"""All CVEs for vendor:product, cached as raw pages."""
|
| 218 |
-
vendor, product = cpe.split(":", 1)
|
| 219 |
path = os.path.join(CACHE, "nvd", re.sub(r"[^A-Za-z0-9_.-]", "_", cpe) + ".json")
|
| 220 |
if os.path.exists(path):
|
| 221 |
with open(path, encoding="utf-8") as handle:
|
|
@@ -223,7 +223,7 @@ def nvd_query(cpe):
|
|
| 223 |
os.makedirs(os.path.dirname(path), exist_ok=True)
|
| 224 |
items, start = [], 0
|
| 225 |
while True:
|
| 226 |
-
params = urllib.parse.urlencode({"virtualMatchString": f"cpe:2.3:
|
| 227 |
url = f"{NVD_URL}?{params}".replace("noRejected=", "noRejected")
|
| 228 |
for attempt in range(5):
|
| 229 |
try:
|
|
@@ -273,7 +273,7 @@ def build_nvd(products, stats):
|
|
| 273 |
for product in products:
|
| 274 |
ranges = {}
|
| 275 |
for cpe in product["cpe"]:
|
| 276 |
-
vendor, name = cpe.split(":"
|
| 277 |
for item in nvd_query(cpe):
|
| 278 |
cve = item["cve"]
|
| 279 |
severity = nvd_severity(cve)
|
|
@@ -288,10 +288,10 @@ def build_nvd(products, stats):
|
|
| 288 |
exact = "" if version in ("*", "-") else version.replace("\\", "")
|
| 289 |
row = (cve["id"], severity, match.get("versionStartIncluding", ""), match.get("versionStartExcluding", ""),
|
| 290 |
match.get("versionEndIncluding", ""), match.get("versionEndExcluding", ""), exact)
|
| 291 |
-
if version == "-" or (not exact and not
|
| 292 |
-
continue # "all versions"
|
| 293 |
ranges[row] = True
|
| 294 |
-
apps.append({"id": product["id"], "name": product["name"], "windows": product.get("windows", ""), "macos": product.get("macos", ""),
|
| 295 |
"cves": sorted(list(r) for r in ranges)})
|
| 296 |
stats["nvd"][product["id"]] = len({r[0] for r in ranges})
|
| 297 |
if not ranges:
|
|
@@ -384,6 +384,20 @@ def main():
|
|
| 384 |
"counts": {"releases": stats["osv"], "apps": stats["nvd"], "kev": len(bundle["kev"]), "epss": len(bundle["epss"])},
|
| 385 |
"signature": None,
|
| 386 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
with open(os.path.join(OUT, "manifest.json"), "w", encoding="utf-8") as handle:
|
| 388 |
json.dump(manifest, handle, indent=2)
|
| 389 |
log(f"\nWrote vulndb/out/{name}: {len(data) / 1048576:.1f} MB compressed ({len(raw) / 1048576:.1f} MB raw)")
|
|
|
|
| 215 |
# --- NVD (desktop applications) ----------------------------------------------------------------
|
| 216 |
def nvd_query(cpe):
|
| 217 |
"""All CVEs for vendor:product, cached as raw pages."""
|
| 218 |
+
part, vendor, product = cpe.split(":", 2) if cpe.count(":") >= 2 and cpe[1] == ":" else ("a", *cpe.split(":", 1))
|
| 219 |
path = os.path.join(CACHE, "nvd", re.sub(r"[^A-Za-z0-9_.-]", "_", cpe) + ".json")
|
| 220 |
if os.path.exists(path):
|
| 221 |
with open(path, encoding="utf-8") as handle:
|
|
|
|
| 223 |
os.makedirs(os.path.dirname(path), exist_ok=True)
|
| 224 |
items, start = [], 0
|
| 225 |
while True:
|
| 226 |
+
params = urllib.parse.urlencode({"virtualMatchString": f"cpe:2.3:{part}:{vendor}:{product}", "startIndex": start, "noRejected": ""})
|
| 227 |
url = f"{NVD_URL}?{params}".replace("noRejected=", "noRejected")
|
| 228 |
for attempt in range(5):
|
| 229 |
try:
|
|
|
|
| 273 |
for product in products:
|
| 274 |
ranges = {}
|
| 275 |
for cpe in product["cpe"]:
|
| 276 |
+
vendor, name = cpe.split(":")[-2:]
|
| 277 |
for item in nvd_query(cpe):
|
| 278 |
cve = item["cve"]
|
| 279 |
severity = nvd_severity(cve)
|
|
|
|
| 288 |
exact = "" if version in ("*", "-") else version.replace("\\", "")
|
| 289 |
row = (cve["id"], severity, match.get("versionStartIncluding", ""), match.get("versionStartExcluding", ""),
|
| 290 |
match.get("versionEndIncluding", ""), match.get("versionEndExcluding", ""), exact)
|
| 291 |
+
if version == "-" or (not exact and not row[4] and not row[5]):
|
| 292 |
+
continue # no upper bound ("all versions" or "from version X on"): cannot tell fixed from unfixed
|
| 293 |
ranges[row] = True
|
| 294 |
+
apps.append({"id": product["id"], "name": product["name"], "windows": product.get("windows", ""), "macos": product.get("macos", ""), "ios": product.get("ios", ""),
|
| 295 |
"cves": sorted(list(r) for r in ranges)})
|
| 296 |
stats["nvd"][product["id"]] = len({r[0] for r in ranges})
|
| 297 |
if not ranges:
|
|
|
|
| 384 |
"counts": {"releases": stats["osv"], "apps": stats["nvd"], "kev": len(bundle["kev"]), "epss": len(bundle["epss"])},
|
| 385 |
"signature": None,
|
| 386 |
}
|
| 387 |
+
# Small bundle for the phone web check: mobile OS records only.
|
| 388 |
+
mobile_apps = [a for a in apps if a.get("ios")]
|
| 389 |
+
mobile_cves = {row[0] for a in mobile_apps for row in a["cves"]}
|
| 390 |
+
mobile = {**{k: bundle[k] for k in ("format", "built_at", "sources")}, "notes": "Phone web check subset: iOS and iPadOS NVD records.",
|
| 391 |
+
"releases": {}, "apps": mobile_apps, "kev": sorted(mobile_cves & kev),
|
| 392 |
+
"epss": {cve: score for cve, score in bundle["epss"].items() if cve in mobile_cves}}
|
| 393 |
+
mobile_name = f"devseis-vulndb-mobile-{today}.json.gz"
|
| 394 |
+
mobile_data = gzip.compress(json.dumps(mobile, separators=(",", ":")).encode("utf-8"), 9, mtime=0)
|
| 395 |
+
with open(os.path.join(OUT, mobile_name), "wb") as handle:
|
| 396 |
+
handle.write(mobile_data)
|
| 397 |
+
manifest["mobile"] = {"file": mobile_name, "size": len(mobile_data), "sha256": hashlib.sha256(mobile_data).hexdigest(), "signature": None}
|
| 398 |
+
for old in os.listdir(OUT):
|
| 399 |
+
if old.startswith("devseis-vulndb-") and old not in (name, mobile_name):
|
| 400 |
+
os.remove(os.path.join(OUT, old))
|
| 401 |
with open(os.path.join(OUT, "manifest.json"), "w", encoding="utf-8") as handle:
|
| 402 |
json.dump(manifest, handle, indent=2)
|
| 403 |
log(f"\nWrote vulndb/out/{name}: {len(data) / 1048576:.1f} MB compressed ({len(raw) / 1048576:.1f} MB raw)")
|
vulndb/products.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
{
|
| 2 |
-
"_comment": "
|
| 3 |
"version": "0.1.0",
|
| 4 |
"products": [
|
| 5 |
{"id": "chrome", "name": "Google Chrome", "cpe": ["google:chrome"], "windows": "^Google Chrome$", "macos": "^Google Chrome$"},
|
|
@@ -34,6 +34,8 @@
|
|
| 34 |
{"id": "tightvnc", "name": "TightVNC", "cpe": ["tightvnc:tightvnc", "glavsoft:tightvnc"], "windows": "^TightVNC$"},
|
| 35 |
{"id": "telegram", "name": "Telegram Desktop", "cpe": ["telegram:telegram_desktop"], "windows": "^Telegram Desktop", "macos": "^Telegram$"},
|
| 36 |
{"id": "safari", "name": "Safari", "cpe": ["apple:safari"], "macos": "^Safari$"},
|
|
|
|
|
|
|
| 37 |
{"id": "iterm2", "name": "iTerm2", "cpe": ["iterm2:iterm2"], "macos": "^iTerm$"}
|
| 38 |
]
|
| 39 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"_comment": "Applications (and mobile OSes) matched against NVD. Regex per OS family on the program name (windows), app bundle name without .app (macos), or \"iOS\"/\"iPadOS\" (ios, from the mobile web check). cpe: NVD [part:]vendor:product, part defaults to a. Keep regexes tight: a wrong match produces false findings.",
|
| 3 |
"version": "0.1.0",
|
| 4 |
"products": [
|
| 5 |
{"id": "chrome", "name": "Google Chrome", "cpe": ["google:chrome"], "windows": "^Google Chrome$", "macos": "^Google Chrome$"},
|
|
|
|
| 34 |
{"id": "tightvnc", "name": "TightVNC", "cpe": ["tightvnc:tightvnc", "glavsoft:tightvnc"], "windows": "^TightVNC$"},
|
| 35 |
{"id": "telegram", "name": "Telegram Desktop", "cpe": ["telegram:telegram_desktop"], "windows": "^Telegram Desktop", "macos": "^Telegram$"},
|
| 36 |
{"id": "safari", "name": "Safari", "cpe": ["apple:safari"], "macos": "^Safari$"},
|
| 37 |
+
{"id": "ios", "name": "iOS", "cpe": ["o:apple:iphone_os"], "ios": "^iOS$"},
|
| 38 |
+
{"id": "ipados", "name": "iPadOS", "cpe": ["o:apple:ipados"], "ios": "^iPadOS$"},
|
| 39 |
{"id": "iterm2", "name": "iTerm2", "cpe": ["iterm2:iterm2"], "macos": "^iTerm$"}
|
| 40 |
]
|
| 41 |
}
|
vulndb/sign-bundle.mjs
CHANGED
|
@@ -33,6 +33,11 @@ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
| 33 |
const sha = createHash('sha256').update(readFileSync(path.join(out, manifest.file))).digest('hex');
|
| 34 |
if (sha !== manifest.sha256) throw new Error(`${manifest.file} does not match manifest.json; rebuild the bundle`);
|
| 35 |
manifest.signature = sign(null, Buffer.from(sha), privateKey).toString('base64');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
manifest.signed_at = new Date().toISOString().replace(/\.\d+Z$/, 'Z');
|
| 37 |
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
| 38 |
console.log(`Signed ${manifest.file} (sha256 ${sha.slice(0, 16)}...)`);
|
|
|
|
| 33 |
const sha = createHash('sha256').update(readFileSync(path.join(out, manifest.file))).digest('hex');
|
| 34 |
if (sha !== manifest.sha256) throw new Error(`${manifest.file} does not match manifest.json; rebuild the bundle`);
|
| 35 |
manifest.signature = sign(null, Buffer.from(sha), privateKey).toString('base64');
|
| 36 |
+
if (manifest.mobile) {
|
| 37 |
+
const mobileSha = createHash('sha256').update(readFileSync(path.join(out, manifest.mobile.file))).digest('hex');
|
| 38 |
+
if (mobileSha !== manifest.mobile.sha256) throw new Error(`${manifest.mobile.file} does not match manifest.json; rebuild the bundle`);
|
| 39 |
+
manifest.mobile.signature = sign(null, Buffer.from(mobileSha), privateKey).toString('base64');
|
| 40 |
+
}
|
| 41 |
manifest.signed_at = new Date().toISOString().replace(/\.\d+Z$/, 'Z');
|
| 42 |
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
| 43 |
console.log(`Signed ${manifest.file} (sha256 ${sha.slice(0, 16)}...)`);
|