File size: 12,403 Bytes
f1bdfc3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f42ab02
 
 
 
f1bdfc3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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
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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
#!/usr/bin/env python3
"""Build library/knowledge.json: the passages the chat assistant answers from (and cites).

Generated from the catalog and library so the chat never drifts from the audit:
  fix      how to fix / why it matters, per check and OS (check_guidance.json + catalog rule + references)
  control  ISO 27001 / GDPR / EU AI Act control summaries (controls.json)
  tool     AI tool profiles (ai_tools.json) and EU AI Act risk categories
  phone    where a setting is on iPhone / Android (catalog/mobile_questions.json)
  status   what each audit status means
  faq      the auditor itself: what it checks, privacy, install, phone check
Usage: python3 library/build_knowledge.py
"""
import json
import os
import re
import sys

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OS_LABEL = {"windows": "Windows", "linux": "Linux", "macos": "macOS", "ios": "iPhone", "android": "Android"}


def load(*parts):
    with open(os.path.join(ROOT, *parts), encoding="utf-8") as handle:
        return json.load(handle)


def read_conf(path):
    conf = {}
    for line in open(os.path.join(ROOT, path), encoding="utf-8"):
        if "=" in line and not line.lstrip().startswith("#"):
            key, value = line.split("=", 1)
            conf[key.strip()] = value.strip().strip('"')
    return conf


def fill(text, baseline):
    """Same placeholder rules as the report builder (training/build_dataset.py), including derived values."""
    sys.path.insert(0, os.path.join(ROOT, "training"))
    from build_dataset import fill as report_fill
    return report_fill(text, baseline)


FAQ = [
    ("What the Devseis Endpoint Auditor is",
     "The Devseis Endpoint Auditor is a free, open-source, read-only check of a computer or phone against ISO 27001, GDPR "
     "technical controls and the EU AI Act. It reads security settings, compares them with a baseline, matches installed "
     "software against known vulnerabilities, and an AI auditor that runs on the device writes a report with findings, "
     "risk levels and fix steps. It is not a certification and not legal advice."),
    ("Does the audit change anything?",
     "No. The audit only reads settings such as password rules, firewall, disk encryption and installed software. It never "
     "changes them. Administrator rights are requested because some settings can only be read by an administrator."),
    ("Privacy: where do audit results go?",
     "Nowhere. Evidence and reports stay on the device. The app downloads only two things: the AI auditor model and a signed "
     "list of known vulnerabilities. Nothing about the device is uploaded. Chat history is stored only in this browser."),
    ("How to run an audit of a computer",
     "A web page cannot read a computer's settings, so computers are audited with the free Devseis desktop app for Mac, "
     "Windows (beta) and Linux. Download it from the Download page, install it, click Run audit and enter the computer "
     "password once. The first run downloads the AI auditor; the audit then takes about 5 to 15 minutes and ends with a PDF report."),
    ("How to install on a Mac",
     "Open the downloaded .dmg file and drag Devseis Endpoint Auditor to Applications. The first time, macOS says it could not "
     "verify the app because it is not yet notarised: click Done, then open System Settings, Privacy & Security, and click "
     "Open Anyway. After that it opens normally. It needs macOS 13 or newer."),
    ("How to install on Windows",
     "Run the downloaded setup .exe. Windows SmartScreen shows 'Windows protected your PC' because the app is not yet "
     "code-signed: click More info, then Run anyway. Click Next, Install and Finish. When the audit starts, click Yes in the "
     "User Account Control prompt. The Windows version is a beta."),
    ("How to install on Linux",
     "On Ubuntu, Debian, Linux Mint or Pop!_OS, double-click the .deb file and click Install. On other distributions, make "
     "the AppImage executable (Properties, Permissions, Allow executing file as program) and double-click it. The audit asks "
     "for the password with the normal desktop prompt."),
    ("How the phone check works",
     "Phones are checked in the browser: open the Devseis page on the iPhone or Android phone and start the phone check. The "
     "browser detects what it can (system version, whether a screen lock is set up) and asks the rest with help on where to "
     "look in Settings. Every result is labelled detected or self-reported. The report can be saved or shared as a PDF."),
    ("What self-reported and detected mean",
     "Detected means the browser or the app read the value itself. Self-reported means the user answered a question; the "
     "value was not verified on the device. Phone checks are mostly self-reported because browsers cannot read phone settings."),
    ("What is in the audit report",
     "The report has a compliance score, the most important gaps, and for each problem: what was found, why it matters, the "
     "risk level, step-by-step fixes for that operating system, how to verify the fix, and the related ISO 27001, GDPR and EU "
     "AI Act references. It also lists AI tools found and their EU AI Act risk class."),
    ("Does the auditor need internet?",
     "Only the first time, to download the AI auditor and the vulnerability list. After that audits and chat work offline. "
     "Pending security updates can only be checked online because they ask the vendor."),
    ("Settings for organisations",
     "Before running audits, an organisation can edit baseline.conf (approved administrator accounts, approved AI tools, "
     "limits such as screen lock within 5 minutes) and organisational.conf (answers to 8 organisational questions such as "
     "record of processing, DPIA and breach procedure). The desktop app keeps them in its settings folder."),
    ("The Devseis AI auditor model",
     "The assistant and the report writer are a small language model fine-tuned by Devseis from Qwen2.5 and run on the "
     "device with WebLLM. Audit verdicts are decided by rules in code, not by the model; the model explains, prioritises and "
     "writes, and every report answer is checked against the evidence before it is used."),
    ("Who is Arx?",
     "Arx is the name of the Devseis security assistant and endpoint auditor (from the Latin for fortress). Arx answers "
     "questions about securing computers and phones, ISO 27001, GDPR and the EU AI Act, and audits devices: computers with the "
     "Devseis Endpoint Auditor desktop app, phones with the phone check in the browser. Arx runs on the device; nothing is uploaded."),
]


def main():
    catalog = load("catalog", "checks.json")
    guidance = load("library", "check_guidance.json")
    controls = load("library", "controls.json")
    tools = load("library", "ai_tools.json")
    phone = load("catalog", "mobile_questions.json")
    baseline = read_conf("baseline.conf")
    refs = catalog["references"]
    ctl = {(c["framework"], c["id"]): c for c in controls["controls"]}
    chunks = []

    def add(cid, kind, title, text, os_family=None, check=None):
        chunks.append({"id": cid, "kind": kind, "title": title, "text": re.sub(r"\s+", " ", text).strip(),
                       **({"os": os_family} if os_family else {}), **({"check": check} if check else {})})

    def ref_text(check):
        out = []
        for key, label in (("iso27001", "ISO 27001:2022"), ("gdpr", "GDPR"), ("ai_act", "EU AI Act")):
            for r in check["controls"][key]:
                title = (ctl.get((label, r)) or {}).get("title") or refs[key].get(r, "")
                out.append(f"{label} {r}" + (f" ({title})" if title else ""))
        return "; ".join(out)

    for check in catalog["checks"]:
        if check["section"] == "org":
            g = guidance["checks"].get(guidance["aliases"].get(check["id"], check["id"]))
            add(f"fix:{check['id']}", "fix", check["name"],
                f"{check['name']} is an organisational control, answered once by the organisation, not read from a device. "
                f"{g['why'] if g else ''} Rule: {check['rule']} Related: {ref_text(check)}.", check=check["id"])
            continue
        g = guidance["checks"].get(guidance["aliases"].get(check["id"], check["id"]))
        if not g:
            continue
        for os_family in check["os"]:
            name = (check.get("names") or {}).get(os_family, check["name"])
            steps = g["fix"].get("all", []) + g["fix"].get(os_family, [])
            text = (f"{name} on {OS_LABEL[os_family]}. Why it matters: {g['why']} Risk if missing: {g['risk']['level']} - "
                    f"{g['risk']['reason']} Requirement: {fill(check['rule'], baseline)} How to fix on {OS_LABEL[os_family]}: "
                    + " ".join(f"({i}) {fill(s, baseline)}" for i, s in enumerate(steps, 1))
                    + f" How to verify: {fill(g['verify'], baseline)} Related controls: {ref_text(check)}.")
            add(f"fix:{check['id']}:{os_family}", "fix", f"{name} on {OS_LABEL[os_family]}", text, os_family, check["id"])

    for c in controls["controls"]:
        add(f"control:{c['framework']}:{c['id']}", "control", f"{c['framework']} {c['id']} {c['title']}",
            f"{c['framework']} {c['id']} ({c['title']}): {c['summary']} An auditor looks for: {'; '.join(c.get('auditor_looks_for', []))}. "
            f"On endpoints: {c.get('endpoint_relevance', '')}")

    for name, t in tools["tools"].items():
        add(f"tool:{name}", "tool", f"{name} (AI tool)",
            f"{name}: {t['kind']}, provider {t['provider']}, data location: {t['data_location']}. Default EU AI Act risk class "
            f"for general office use: {t['default_risk']}. GDPR: {t['gdpr_notes']} Whether it is allowed at work depends on the "
            f"organisation's approved AI tools list and on the purpose: some purposes make any AI tool high-risk or prohibited.")
    rules = tools["purpose_rules"]
    for key, title in (("prohibited", "EU AI Act prohibited AI practices"), ("high_risk_examples", "EU AI Act high-risk AI uses"),
                       ("limited_risk", "EU AI Act limited-risk AI (transparency duties)"), ("minimal_risk", "EU AI Act minimal-risk AI")):
        items = rules[key] if isinstance(rules[key], list) else [rules[key]]
        add(f"aiact:{key}", "control", title, f"{title}: " + "; ".join(items) + ".")

    for check_id, per_os in phone["checks"].items():
        for os_family, spec in per_os.items():
            name = next(((c.get("names") or {}).get(os_family, c["name"]) for c in catalog["checks"] if c["id"] == check_id), check_id)
            add(f"phone:{check_id}:{os_family}", "phone", f"Where to find {name} on {OS_LABEL[os_family]}",
                f"{name} on {OS_LABEL[os_family]}: {spec['question']} Where to look: {spec['where']}", os_family, check_id)

    notes = guidance["status_notes"]
    add("status:all", "status", "What the audit statuses mean",
        "Compliant: the setting meets the baseline. Non-Compliant: it does not meet the baseline. "
        f"Pending: {notes['Pending']} NotApplicable: {notes['NotApplicable']} Error: {notes['Error']}")

    counts = {o: sum(o in c["os"] for c in catalog["checks"]) for o in OS_LABEL}
    add("faq:checks", "faq", "How many checks the audit runs",
        "The audit catalog has " + str(len(catalog["checks"])) + " checks: " + ", ".join(f"{OS_LABEL[o]} {n}" for o, n in counts.items())
        + ". They cover passwords and lockout, malware protection, firewall, screen lock, USB storage, administrator accounts, "
          "disk encryption, updates and known vulnerabilities, GDPR technical controls (backups, logging, remote access, "
          "telemetry and more), EU AI Act controls (AI tools found, shadow AI, risk classification) and 8 organisational controls.")
    for i, (title, text) in enumerate(FAQ):
        add(f"faq:{i}", "faq", title, text)

    out = {"version": catalog["catalog_version"], "chunks": chunks}
    with open(os.path.join(ROOT, "library", "knowledge.json"), "w", encoding="utf-8") as handle:
        json.dump(out, handle, ensure_ascii=False, indent=0)
    kinds = {}
    for c in chunks:
        kinds[c["kind"]] = kinds.get(c["kind"], 0) + 1
    print(f"knowledge.json: {len(chunks)} passages {kinds}")


if __name__ == "__main__":
    main()