feat(proxy): apt proxy audit + doctor mode; reinstall-from-clone = update

apt-audit.js scans EVERY file in apt.conf.d for Acquire::*::Proxy, classifying:
- creds: embedded userinfo (plaintext leak + stale-cred hammering risk)
- overrides: sorts after 80proxy-bridge so apt last-wins REPLACES the bridge URL
- external: points at a non-bridge proxy
Installer Legacy section alerts + fixes (backup kept: *.disabled-by-proxy-bridge).
New 'install.js doctor [--fix]': audits apt, bashrc, snap, service drop-ins,
service state, keyring presence; exit 1 when issues found.

Update semantics: deploy writes .installed-from (git commit + file hashes);
runtime JSONs are never created if present and never overwritten; tests prove
3 consecutive installs leave runtime state byte-identical. Downgrade protection
comes from the visible marker + diff reporting, never silent clobbering.

Tests: 6 audit + 2 installer (update-semantics, doctor) — suite now 23/23.
This commit is contained in:
tke committed 2026-09-23 16:07:53 +02:00
1 parent a626392eb1
commit a23fa00dfb
4 files changed
+339 -15

No files matched your search

+84
View File
@@ -0,0 +1,84 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const { scanAptProxies, summarizeAptFindings } = require('./apt-audit.js');
function mkAptDir(t, files) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'apt-audit-'));
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
for (const [name, content] of Object.entries(files)) {
fs.writeFileSync(path.join(dir, name), content);
}
return dir;
}
const BRIDGE_CONF = 'Acquire::http::Proxy "http://127.0.0.1:8888/";\nAcquire::https::Proxy "http://127.0.0.1:8888/";\n';
test('clean setup: only 80proxy-bridge with bridge URL → no threats', t => {
const dir = mkAptDir(t, {
'20archive': '// no proxies here\n',
'80proxy-bridge': BRIDGE_CONF,
'99update-notifier': '// unrelated\n',
});
const { findings } = scanAptProxies(dir);
const summary = summarizeAptFindings(findings);
assert.equal(summary.bridgePresent, true);
assert.deepEqual(summary.threats, []);
});
test('legacy 80proxy with embedded creds is flagged creds even when sorting before ours', t => {
const dir = mkAptDir(t, {
'80proxy': 'Acquire::http::Proxy "http://user:p%40ss@proxy.corp:8080/";\n',
'80proxy-bridge': BRIDGE_CONF,
});
const { findings } = scanAptProxies(dir);
const credFinding = findings.find(f => f.name === '80proxy');
assert.equal(credFinding.severity, 'creds');
assert.equal(summarizeAptFindings(findings).threats.length, 1);
});
test('a later-sorting file with an external proxy overrides the bridge (apt last-wins)', t => {
const dir = mkAptDir(t, {
'80proxy-bridge': BRIDGE_CONF,
'99custom': 'Acquire::http::Proxy "http://other.proxy:3128/";\n',
});
const { findings } = scanAptProxies(dir);
const f99 = findings.find(f => f.name === '99custom');
assert.equal(f99.severity, 'overrides');
assert.equal(f99.sortsAfterBridge, true);
assert.equal(summarizeAptFindings(findings).threats.length, 1);
});
test('earlier-sorting external proxy does not override but is still reported external', t => {
const dir = mkAptDir(t, {
'10proxy': 'Acquire::https::Proxy "http://other.proxy:3128/";\n',
'80proxy-bridge': BRIDGE_CONF,
});
const { findings } = scanAptProxies(dir);
const f10 = findings.find(f => f.name === '10proxy');
assert.equal(f10.severity, 'external');
assert.equal(f10.sortsAfterBridge, false);
});
test('missing directory returns an error instead of crashing', t => {
const { findings, error } = scanAptProxies('/nonexistent-apt-dir-xyz');
assert.deepEqual(findings, []);
assert.ok(error);
});
test('bridge file missing entirely: bridgePresent false, all proxy files reported', t => {
const dir = mkAptDir(t, {
'50proxy': 'Acquire::http::Proxy "http://some.proxy:8080/";\n',
});
const { findings } = scanAptProxies(dir);
const summary = summarizeAptFindings(findings);
assert.equal(summary.bridgePresent, false);
assert.equal(summary.threats.length, 1);
assert.equal(summary.threats[0].severity, 'external'); // effective config; not "overriding" a bridge that isn't there
// A cred-bearing file must still be flagged creds even without a bridge file:
const dir2 = mkAptDir(t, { '99bad': 'Acquire::http::Proxy "http://u:p@proxy:8080/";\n' });
const { findings: f2 } = scanAptProxies(dir2);
assert.equal(f2[0].severity, 'creds');
});