fullcourtdefense-cli 1.15.8 → 1.15.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/daemon.js +3 -1
- package/dist/commands/discover.js +34 -4
- package/dist/commands/discoverPaths.d.ts +19 -0
- package/dist/commands/discoverPaths.js +74 -0
- package/dist/commands/discoverSecrets.d.ts +1 -0
- package/dist/commands/discoverSecrets.js +33 -2
- package/dist/runtimeConfig.d.ts +16 -0
- package/dist/runtimeConfig.js +37 -3
- package/dist/version.json +1 -1
- package/package.json +1 -1
package/dist/commands/daemon.js
CHANGED
|
@@ -135,7 +135,9 @@ function logFile() {
|
|
|
135
135
|
/** Spawn `discover --upload --surface all --silent` and wait for it to finish. */
|
|
136
136
|
function runDiscoverSweep(timeoutMs = 300_000) {
|
|
137
137
|
return new Promise((resolve, reject) => {
|
|
138
|
-
|
|
138
|
+
// --scan-root auto sweeps the default dev roots; admin-selected folders from
|
|
139
|
+
// the console are merged in by discover itself (cached runtime bundle).
|
|
140
|
+
const child = (0, child_process_1.spawn)(process.execPath, [cliEntry(), 'discover', '--upload', '--surface', 'all', '--silent', '--scan-root', 'auto'], {
|
|
139
141
|
windowsHide: true,
|
|
140
142
|
stdio: 'ignore',
|
|
141
143
|
// Daemon/scheduled-task cwd is C:\WINDOWS\system32 — scan from the user's
|
|
@@ -45,6 +45,7 @@ const discoverProxy_1 = require("./discoverProxy");
|
|
|
45
45
|
const discoverSchedule_1 = require("./discoverSchedule");
|
|
46
46
|
const mcpGateway_1 = require("./mcpGateway");
|
|
47
47
|
const discoverPaths_1 = require("./discoverPaths");
|
|
48
|
+
const runtimeConfig_1 = require("../runtimeConfig");
|
|
48
49
|
const knownMcpServers_1 = require("./knownMcpServers");
|
|
49
50
|
const discoverAgentFiles_1 = require("./discoverAgentFiles");
|
|
50
51
|
const discoverBlastRadius_1 = require("./discoverBlastRadius");
|
|
@@ -821,9 +822,18 @@ async function discoverCommand(args, config) {
|
|
|
821
822
|
// Computed once and shared across MCP, secrets, and agent-file scans so the
|
|
822
823
|
// whole posture references every mapped folder. Without the flag it is empty
|
|
823
824
|
// and behavior is exactly the known-locations scan.
|
|
824
|
-
|
|
825
|
+
// Admin-chosen folders (picked in the console per machine, delivered via the
|
|
826
|
+
// runtime bundle) are always merged in, so every scheduled/daemon/manual scan
|
|
827
|
+
// covers them without extra flags. Default folders the admin explicitly
|
|
828
|
+
// removed in the console are dropped from the sweep.
|
|
829
|
+
const { extraScanRoots: adminScanRoots, disabledScanRoots } = (0, runtimeConfig_1.getCachedScanRootOverrides)();
|
|
830
|
+
const disabledRootSet = new Set(disabledScanRoots.map(root => path.resolve(root).toLowerCase()));
|
|
831
|
+
const isDisabledRoot = (root) => disabledRootSet.has(path.resolve(root).toLowerCase());
|
|
832
|
+
const scanRoots = [...new Set([...(0, discoverPaths_1.resolveScanRoots)(args.scanRoot), ...adminScanRoots])]
|
|
833
|
+
.filter(root => !isDisabledRoot(root));
|
|
825
834
|
const sweep = scanRoots.length > 0 ? (0, discoverPaths_1.scanRootsForProjectConfigs)(scanRoots) : { candidates: [], scannedDirs: 0 };
|
|
826
|
-
|
|
835
|
+
// Admin roots are scanned for env/secrets even when no MCP config lives there.
|
|
836
|
+
const sweepProjectRoots = [...new Set([...deriveProjectRoots(sweep.candidates), ...adminScanRoots])];
|
|
827
837
|
const knownConfigCandidates = candidateConfigPaths(cwd, args.extraPath);
|
|
828
838
|
const secretConfigFiles = [...knownConfigCandidates, ...sweep.candidates]
|
|
829
839
|
.map(candidate => path.resolve(candidate.path))
|
|
@@ -871,7 +881,7 @@ async function discoverCommand(args, config) {
|
|
|
871
881
|
}
|
|
872
882
|
}
|
|
873
883
|
const secrets = surfaces.has('secrets') || surfaces.has('posture')
|
|
874
|
-
? (0, discoverSecrets_1.scanSecrets)({ cwd, extraRoots: sweepProjectRoots, configFiles: secretConfigFiles })
|
|
884
|
+
? (0, discoverSecrets_1.scanSecrets)({ cwd, extraRoots: sweepProjectRoots, disabledRoots: disabledScanRoots, configFiles: secretConfigFiles })
|
|
875
885
|
: undefined;
|
|
876
886
|
const agentFiles = surfaces.has('agent-files') || surfaces.has('posture')
|
|
877
887
|
? (0, discoverAgentFiles_1.scanAgentFiles)({ cwd, extraRoots: sweepProjectRoots })
|
|
@@ -886,6 +896,23 @@ async function discoverCommand(args, config) {
|
|
|
886
896
|
: undefined;
|
|
887
897
|
const host = buildHostMetadata(args.userEmail, deep && found.some(s => s.probeMode === 'deep') ? 'deep' : 'config');
|
|
888
898
|
const home = os.homedir();
|
|
899
|
+
// Effective roots the posture scan covered — proof of coverage for the console.
|
|
900
|
+
// Admin-removed defaults are excluded here too, so the console mirrors reality.
|
|
901
|
+
const defaultEnvRoots = [home, cwd, path.join(home, 'dev'), path.join(home, 'repos'), path.join(home, 'projects'), path.join(home, 'Documents'), path.join(home, 'code')]
|
|
902
|
+
.map(root => path.resolve(root));
|
|
903
|
+
const adminRootSet = new Set(adminScanRoots.map(root => path.resolve(root).toLowerCase()));
|
|
904
|
+
const scannedRoots = [
|
|
905
|
+
...[...new Set(defaultEnvRoots)].filter(root => fs.existsSync(root) && !isDisabledRoot(root)).map(root => ({ path: root, source: 'default' })),
|
|
906
|
+
...adminScanRoots.map(root => path.resolve(root)).filter(root => fs.existsSync(root)).map(root => ({ path: root, source: 'admin' })),
|
|
907
|
+
...(0, discoverPaths_1.resolveScanRoots)(args.scanRoot)
|
|
908
|
+
.map(root => path.resolve(root))
|
|
909
|
+
.filter(root => fs.existsSync(root) && !adminRootSet.has(root.toLowerCase()) && !isDisabledRoot(root))
|
|
910
|
+
.map(root => ({ path: root, source: 'default' })),
|
|
911
|
+
].filter((entry, index, list) => list.findIndex(other => other.path.toLowerCase() === entry.path.toLowerCase()) === index);
|
|
912
|
+
// Shallow folder tree for the console folder picker (names only, no contents).
|
|
913
|
+
const folderCatalog = surfaces.has('posture') || surfaces.has('secrets')
|
|
914
|
+
? (0, discoverPaths_1.buildFolderCatalog)({ extraRoots: adminScanRoots })
|
|
915
|
+
: undefined;
|
|
889
916
|
const uploadExtras = {
|
|
890
917
|
secrets,
|
|
891
918
|
agentFiles,
|
|
@@ -895,6 +922,8 @@ async function discoverCommand(args, config) {
|
|
|
895
922
|
surfaces: [...surfaces],
|
|
896
923
|
workingDirectory: cwd,
|
|
897
924
|
mcpConfigPaths: scanned.map(s => ({ ...s, exists: fs.existsSync(s.path) })),
|
|
925
|
+
scannedRoots,
|
|
926
|
+
folderCatalog,
|
|
898
927
|
included: [
|
|
899
928
|
cwd === rawCwd
|
|
900
929
|
? `Current command folder: ${cwd}`
|
|
@@ -903,13 +932,14 @@ async function discoverCommand(args, config) {
|
|
|
903
932
|
`User-level AI rules, skills, hooks, and instruction files under ${home}`,
|
|
904
933
|
`Credential stores and shell history under ${home}`,
|
|
905
934
|
`Environment files under ${cwd}, ${path.join(home, 'dev')}, ${path.join(home, 'repos')}, ${path.join(home, 'projects')}, ${path.join(home, 'Documents')}, and ${path.join(home, 'code')}`,
|
|
935
|
+
...adminScanRoots.map(root => `Admin-selected folder: ${root} (and subfolders)`),
|
|
906
936
|
],
|
|
907
937
|
excluded: [
|
|
908
938
|
'Unrelated project folders outside the listed roots',
|
|
909
939
|
'Large/generated dependency and build folders such as node_modules, .git, dist, build, vendor, caches, and temp folders',
|
|
910
940
|
'Full disk contents, browser profiles, email/chat archives, binary files, and operating-system folders',
|
|
911
941
|
],
|
|
912
|
-
note: 'Desktop posture is a targeted AI-development exposure scan, not a full-device forensic scan.
|
|
942
|
+
note: 'Desktop posture is a targeted AI-development exposure scan, not a full-device forensic scan. Add folders from the machine page to expand coverage.',
|
|
913
943
|
},
|
|
914
944
|
};
|
|
915
945
|
async function maybeUpload() {
|
|
@@ -29,6 +29,25 @@ export declare function scanRootsForProjectConfigs(roots: string[], options?: {
|
|
|
29
29
|
candidates: ConfigPathCandidate[];
|
|
30
30
|
scannedDirs: number;
|
|
31
31
|
};
|
|
32
|
+
export interface FolderCatalogEntry {
|
|
33
|
+
path: string;
|
|
34
|
+
/** Depth relative to its walk root (home or a dev root) — lets the UI indent. */
|
|
35
|
+
depth: number;
|
|
36
|
+
/** True when the folder has at least one visible subfolder (UI shows an expander). */
|
|
37
|
+
hasChildren: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Shallow, bounded folder tree of the machine for the console folder picker:
|
|
41
|
+
* the user home plus every default dev root, walked a few levels deep, names
|
|
42
|
+
* only. The admin expands this tree in the dashboard and picks folders to add
|
|
43
|
+
* to the posture scan — so the picker always reflects the machine's REAL
|
|
44
|
+
* structure without uploading anything sensitive (no files, no contents).
|
|
45
|
+
*/
|
|
46
|
+
export declare function buildFolderCatalog(options?: {
|
|
47
|
+
maxDepth?: number;
|
|
48
|
+
maxEntries?: number;
|
|
49
|
+
extraRoots?: string[];
|
|
50
|
+
}): FolderCatalogEntry[];
|
|
32
51
|
/** Resolve the --scan-root flag: comma-separated paths, or "auto" for common dev folders. */
|
|
33
52
|
export declare function resolveScanRoots(flag: string | undefined): string[];
|
|
34
53
|
/** Include config paths for apps that exist on disk even when the MCP file is not created yet. */
|
|
@@ -40,6 +40,7 @@ exports.claudeDesktopInstallTargets = claudeDesktopInstallTargets;
|
|
|
40
40
|
exports.candidateConfigPaths = candidateConfigPaths;
|
|
41
41
|
exports.defaultScanRoots = defaultScanRoots;
|
|
42
42
|
exports.scanRootsForProjectConfigs = scanRootsForProjectConfigs;
|
|
43
|
+
exports.buildFolderCatalog = buildFolderCatalog;
|
|
43
44
|
exports.resolveScanRoots = resolveScanRoots;
|
|
44
45
|
exports.discoverScanTargets = discoverScanTargets;
|
|
45
46
|
const fs = __importStar(require("fs"));
|
|
@@ -281,6 +282,79 @@ function scanRootsForProjectConfigs(roots, options = {}) {
|
|
|
281
282
|
}
|
|
282
283
|
return { candidates, scannedDirs };
|
|
283
284
|
}
|
|
285
|
+
/** Non-dev home folders that only add noise to the console folder picker. */
|
|
286
|
+
const CATALOG_SKIP_HOME_DIRS = new Set([
|
|
287
|
+
'appdata', 'application data', 'local settings', 'cookies', 'nethood', 'printhood',
|
|
288
|
+
'recent', 'sendto', 'start menu', 'templates', 'searches', 'links', 'saved games',
|
|
289
|
+
'3d objects', 'contacts', 'favorites', 'music', 'videos', 'pictures', 'library',
|
|
290
|
+
'movies', 'public',
|
|
291
|
+
]);
|
|
292
|
+
/**
|
|
293
|
+
* Shallow, bounded folder tree of the machine for the console folder picker:
|
|
294
|
+
* the user home plus every default dev root, walked a few levels deep, names
|
|
295
|
+
* only. The admin expands this tree in the dashboard and picks folders to add
|
|
296
|
+
* to the posture scan — so the picker always reflects the machine's REAL
|
|
297
|
+
* structure without uploading anything sensitive (no files, no contents).
|
|
298
|
+
*/
|
|
299
|
+
function buildFolderCatalog(options = {}) {
|
|
300
|
+
const maxDepth = options.maxDepth ?? 3;
|
|
301
|
+
const maxEntries = options.maxEntries ?? 600;
|
|
302
|
+
const home = os.homedir();
|
|
303
|
+
const entries = [];
|
|
304
|
+
const seen = new Set();
|
|
305
|
+
const listVisibleSubdirs = (dir, atHomeLevel) => {
|
|
306
|
+
let dirents;
|
|
307
|
+
try {
|
|
308
|
+
dirents = fs.readdirSync(dir, { withFileTypes: true });
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return [];
|
|
312
|
+
}
|
|
313
|
+
return dirents
|
|
314
|
+
.filter(entry => {
|
|
315
|
+
if (!entry.isDirectory())
|
|
316
|
+
return false;
|
|
317
|
+
const name = entry.name.toLowerCase();
|
|
318
|
+
if (entry.name.startsWith('.') || entry.name.startsWith('$'))
|
|
319
|
+
return false;
|
|
320
|
+
if (SCAN_SKIP_DIRS.has(name))
|
|
321
|
+
return false;
|
|
322
|
+
if (atHomeLevel && CATALOG_SKIP_HOME_DIRS.has(name))
|
|
323
|
+
return false;
|
|
324
|
+
return true;
|
|
325
|
+
})
|
|
326
|
+
.map(entry => path.join(dir, entry.name));
|
|
327
|
+
};
|
|
328
|
+
const walk = (root) => {
|
|
329
|
+
const queue = [{ dir: root, depth: 0 }];
|
|
330
|
+
while (queue.length > 0 && entries.length < maxEntries) {
|
|
331
|
+
const { dir, depth } = queue.shift();
|
|
332
|
+
const key = path.resolve(dir).toLowerCase();
|
|
333
|
+
if (seen.has(key))
|
|
334
|
+
continue;
|
|
335
|
+
seen.add(key);
|
|
336
|
+
const children = listVisibleSubdirs(dir, key === home.toLowerCase());
|
|
337
|
+
entries.push({ path: path.resolve(dir), depth, hasChildren: children.length > 0 });
|
|
338
|
+
if (depth < maxDepth) {
|
|
339
|
+
for (const child of children)
|
|
340
|
+
queue.push({ dir: child, depth: depth + 1 });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
walk(home);
|
|
345
|
+
for (const root of defaultScanRoots())
|
|
346
|
+
walk(root);
|
|
347
|
+
// Admin-chosen folders get catalogued too, so the picker tree keeps expanding
|
|
348
|
+
// beneath paths the admin already selected.
|
|
349
|
+
for (const root of options.extraRoots || []) {
|
|
350
|
+
try {
|
|
351
|
+
if (fs.statSync(root).isDirectory())
|
|
352
|
+
walk(root);
|
|
353
|
+
}
|
|
354
|
+
catch { /* folder removed since selection — skip */ }
|
|
355
|
+
}
|
|
356
|
+
return entries;
|
|
357
|
+
}
|
|
284
358
|
/** Resolve the --scan-root flag: comma-separated paths, or "auto" for common dev folders. */
|
|
285
359
|
function resolveScanRoots(flag) {
|
|
286
360
|
if (!flag?.trim())
|
|
@@ -199,6 +199,28 @@ function scanCredentialStorePosture(filePath, content, category, out, seen) {
|
|
|
199
199
|
detail: '.netrc contains machine login/password material used by CLI tools.',
|
|
200
200
|
});
|
|
201
201
|
}
|
|
202
|
+
if (normalized.endsWith('/.git-credentials') && /https?:\/\/[^:\s/]+:[^@\s]+@/i.test(content)) {
|
|
203
|
+
pushStoreFinding(out, seen, {
|
|
204
|
+
filePath,
|
|
205
|
+
category,
|
|
206
|
+
provider: 'git',
|
|
207
|
+
severity: 'high',
|
|
208
|
+
title: 'Git credentials stored in plaintext',
|
|
209
|
+
detail: '.git-credentials stores repository usernames/tokens unencrypted — anything on this machine (including AI agents) can read and reuse them.',
|
|
210
|
+
remediation: 'Switch to `git config --global credential.helper manager` (Windows/mac keychain) or `cache`, then delete ~/.git-credentials and rotate the exposed tokens.',
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
if (normalized.endsWith('/.gitconfig') && /helper\s*=\s*store\b/i.test(content)) {
|
|
214
|
+
pushStoreFinding(out, seen, {
|
|
215
|
+
filePath,
|
|
216
|
+
category,
|
|
217
|
+
provider: 'git',
|
|
218
|
+
severity: 'medium',
|
|
219
|
+
title: 'Git credential helper set to plaintext "store"',
|
|
220
|
+
detail: 'credential.helper=store writes every git password/token to ~/.git-credentials in plaintext.',
|
|
221
|
+
remediation: 'Use the OS keychain helper instead: `git config --global credential.helper manager` (Windows) / `osxkeychain` (mac) / `libsecret` (Linux).',
|
|
222
|
+
});
|
|
223
|
+
}
|
|
202
224
|
if (normalized.endsWith('/.pypirc') && /\b(?:password|token)\s*=\s*\S+/i.test(content)) {
|
|
203
225
|
pushStoreFinding(out, seen, {
|
|
204
226
|
filePath,
|
|
@@ -317,6 +339,14 @@ function credentialStores(home) {
|
|
|
317
339
|
const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
318
340
|
return [
|
|
319
341
|
{ path: path.join(home, '.aws', 'credentials'), category: 'credential_store', title: 'AWS credentials file' },
|
|
342
|
+
{ path: path.join(home, '.git-credentials'), category: 'credential_store', title: 'Git plaintext credentials' },
|
|
343
|
+
{ path: path.join(home, '.gitconfig'), category: 'credential_store', title: 'Git config' },
|
|
344
|
+
{ path: path.join(appData, 'Code', 'User', 'settings.json'), category: 'credential_store', title: 'VS Code user settings' },
|
|
345
|
+
{ path: path.join(appData, 'Cursor', 'User', 'settings.json'), category: 'credential_store', title: 'Cursor user settings' },
|
|
346
|
+
{ path: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'settings.json'), category: 'credential_store', title: 'VS Code user settings (mac)' },
|
|
347
|
+
{ path: path.join(home, 'Library', 'Application Support', 'Cursor', 'User', 'settings.json'), category: 'credential_store', title: 'Cursor user settings (mac)' },
|
|
348
|
+
{ path: path.join(xdg, 'Code', 'User', 'settings.json'), category: 'credential_store', title: 'VS Code user settings (linux)' },
|
|
349
|
+
{ path: path.join(xdg, 'Cursor', 'User', 'settings.json'), category: 'credential_store', title: 'Cursor user settings (linux)' },
|
|
320
350
|
{ path: path.join(home, '.docker', 'config.json'), category: 'credential_store', title: 'Docker registry auth' },
|
|
321
351
|
{ path: path.join(home, '.npmrc'), category: 'credential_store', title: 'npm auth token' },
|
|
322
352
|
{ path: path.join(home, '.pypirc'), category: 'credential_store', title: 'PyPI auth token' },
|
|
@@ -448,7 +478,8 @@ function scanSecrets(options = {}) {
|
|
|
448
478
|
const home = os.homedir();
|
|
449
479
|
const cwd = options.cwd || process.cwd();
|
|
450
480
|
const maxDepth = options.maxEnvDepth ?? 4;
|
|
451
|
-
const
|
|
481
|
+
const disabledRoots = new Set((options.disabledRoots ?? []).map(root => path.resolve(root).toLowerCase()));
|
|
482
|
+
const extraRoots = (options.extraRoots ?? []).filter(root => !disabledRoots.has(path.resolve(root).toLowerCase()));
|
|
452
483
|
const findings = [];
|
|
453
484
|
const seen = new Set();
|
|
454
485
|
let scannedFiles = 0;
|
|
@@ -483,7 +514,7 @@ function scanSecrets(options = {}) {
|
|
|
483
514
|
path.join(home, 'projects'),
|
|
484
515
|
path.join(home, 'Documents'),
|
|
485
516
|
path.join(home, 'code'),
|
|
486
|
-
].filter(root => fs.existsSync(root))));
|
|
517
|
+
].filter(root => fs.existsSync(root) && !disabledRoots.has(path.resolve(root).toLowerCase()))));
|
|
487
518
|
for (const envFile of collectEnvFiles(envRoots, maxDepth)) {
|
|
488
519
|
if (findings.length >= MAX_FINDINGS)
|
|
489
520
|
break;
|
package/dist/runtimeConfig.d.ts
CHANGED
|
@@ -24,6 +24,10 @@ export interface RuntimeBundle {
|
|
|
24
24
|
failClosed?: boolean;
|
|
25
25
|
/** Admin kill-switch: while true, hooks and the gateway deny ALL actions. */
|
|
26
26
|
suspended?: boolean;
|
|
27
|
+
/** Admin-chosen extra posture scan folders for this machine (from the console). */
|
|
28
|
+
extraScanRoots?: string[];
|
|
29
|
+
/** Default scan folders the admin explicitly removed from this machine's posture sweep. */
|
|
30
|
+
disabledScanRoots?: string[];
|
|
27
31
|
/** One constrained, auditable action queued for the resident daemon. */
|
|
28
32
|
machineAction?: {
|
|
29
33
|
id: string;
|
|
@@ -55,3 +59,15 @@ export interface EffectiveBundle extends RuntimeBundle {
|
|
|
55
59
|
* or a 'default' marker so the caller can apply its local fallback.
|
|
56
60
|
*/
|
|
57
61
|
export declare function getRuntimeBundle(input: FetchBundleInput): Promise<EffectiveBundle>;
|
|
62
|
+
/**
|
|
63
|
+
* Admin scan-folder choices from the most recent cached bundle (any shield):
|
|
64
|
+
* extra folders to add and default folders the admin removed. Read-only and
|
|
65
|
+
* offline — used by `discover` so scheduled/daemon scans honor the console
|
|
66
|
+
* selection without needing credentials plumbed in.
|
|
67
|
+
*/
|
|
68
|
+
export declare function getCachedScanRootOverrides(): {
|
|
69
|
+
extraScanRoots: string[];
|
|
70
|
+
disabledScanRoots: string[];
|
|
71
|
+
};
|
|
72
|
+
/** @deprecated Use getCachedScanRootOverrides(). */
|
|
73
|
+
export declare function getCachedExtraScanRoots(): string[];
|
package/dist/runtimeConfig.js
CHANGED
|
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.getRuntimeBundle = getRuntimeBundle;
|
|
37
|
+
exports.getCachedScanRootOverrides = getCachedScanRootOverrides;
|
|
38
|
+
exports.getCachedExtraScanRoots = getCachedExtraScanRoots;
|
|
37
39
|
const fs = __importStar(require("fs"));
|
|
38
40
|
const os = __importStar(require("os"));
|
|
39
41
|
const path = __importStar(require("path"));
|
|
@@ -70,7 +72,7 @@ async function getRuntimeBundle(input) {
|
|
|
70
72
|
const cached = cache[input.shieldId];
|
|
71
73
|
const fresh = cached && Date.now() - cached.fetchedAt < ttl;
|
|
72
74
|
if (cached && fresh && !input.force) {
|
|
73
|
-
return { mode: cached.mode, version: cached.version, policyHash: cached.policyHash, policyCount: cached.policyCount, pollIntervalMs: cached.pollIntervalMs, failClosed: cached.failClosed, suspended: cached.suspended, machineAction: cached.machineAction, source: 'cache' };
|
|
75
|
+
return { mode: cached.mode, version: cached.version, policyHash: cached.policyHash, policyCount: cached.policyCount, pollIntervalMs: cached.pollIntervalMs, failClosed: cached.failClosed, suspended: cached.suspended, extraScanRoots: cached.extraScanRoots, machineAction: cached.machineAction, source: 'cache' };
|
|
74
76
|
}
|
|
75
77
|
try {
|
|
76
78
|
const headers = { 'Content-Type': 'application/json' };
|
|
@@ -90,7 +92,7 @@ async function getRuntimeBundle(input) {
|
|
|
90
92
|
if (resp.status === 304 && cached) {
|
|
91
93
|
cache[input.shieldId] = { ...cached, fetchedAt: Date.now() };
|
|
92
94
|
writeCacheFile(cache);
|
|
93
|
-
return { mode: cached.mode, version: cached.version, policyHash: cached.policyHash, policyCount: cached.policyCount, pollIntervalMs: cached.pollIntervalMs, failClosed: cached.failClosed, suspended: cached.suspended, machineAction: cached.machineAction, source: 'cache' };
|
|
95
|
+
return { mode: cached.mode, version: cached.version, policyHash: cached.policyHash, policyCount: cached.policyCount, pollIntervalMs: cached.pollIntervalMs, failClosed: cached.failClosed, suspended: cached.suspended, extraScanRoots: cached.extraScanRoots, machineAction: cached.machineAction, source: 'cache' };
|
|
94
96
|
}
|
|
95
97
|
if (resp.ok) {
|
|
96
98
|
const body = await resp.json().catch(() => ({}));
|
|
@@ -103,6 +105,12 @@ async function getRuntimeBundle(input) {
|
|
|
103
105
|
pollIntervalMs: body.data.pollIntervalMs,
|
|
104
106
|
failClosed: typeof body.data.failClosed === 'boolean' ? body.data.failClosed : undefined,
|
|
105
107
|
suspended: body.data.suspended === true,
|
|
108
|
+
extraScanRoots: Array.isArray(body.data.extraScanRoots)
|
|
109
|
+
? body.data.extraScanRoots.filter((root) => typeof root === 'string').slice(0, 20)
|
|
110
|
+
: undefined,
|
|
111
|
+
disabledScanRoots: Array.isArray(body.data.disabledScanRoots)
|
|
112
|
+
? body.data.disabledScanRoots.filter((root) => typeof root === 'string').slice(0, 20)
|
|
113
|
+
: undefined,
|
|
106
114
|
machineAction: body.data.machineAction,
|
|
107
115
|
fetchedAt: Date.now(),
|
|
108
116
|
};
|
|
@@ -114,7 +122,33 @@ async function getRuntimeBundle(input) {
|
|
|
114
122
|
}
|
|
115
123
|
catch { /* fall through to cache / default */ }
|
|
116
124
|
if (cached) {
|
|
117
|
-
return { mode: cached.mode, version: cached.version, policyHash: cached.policyHash, policyCount: cached.policyCount, pollIntervalMs: cached.pollIntervalMs, failClosed: cached.failClosed, suspended: cached.suspended, machineAction: cached.machineAction, source: 'cache' };
|
|
125
|
+
return { mode: cached.mode, version: cached.version, policyHash: cached.policyHash, policyCount: cached.policyCount, pollIntervalMs: cached.pollIntervalMs, failClosed: cached.failClosed, suspended: cached.suspended, extraScanRoots: cached.extraScanRoots, machineAction: cached.machineAction, source: 'cache' };
|
|
118
126
|
}
|
|
119
127
|
return { mode: 'block', version: '', source: 'default' };
|
|
120
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Admin scan-folder choices from the most recent cached bundle (any shield):
|
|
131
|
+
* extra folders to add and default folders the admin removed. Read-only and
|
|
132
|
+
* offline — used by `discover` so scheduled/daemon scans honor the console
|
|
133
|
+
* selection without needing credentials plumbed in.
|
|
134
|
+
*/
|
|
135
|
+
function getCachedScanRootOverrides() {
|
|
136
|
+
const cache = readCacheFile();
|
|
137
|
+
const extra = new Set();
|
|
138
|
+
const disabled = new Set();
|
|
139
|
+
for (const entry of Object.values(cache)) {
|
|
140
|
+
for (const root of entry.extraScanRoots || []) {
|
|
141
|
+
if (typeof root === 'string' && root.trim())
|
|
142
|
+
extra.add(root.trim());
|
|
143
|
+
}
|
|
144
|
+
for (const root of entry.disabledScanRoots || []) {
|
|
145
|
+
if (typeof root === 'string' && root.trim())
|
|
146
|
+
disabled.add(root.trim());
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return { extraScanRoots: [...extra], disabledScanRoots: [...disabled] };
|
|
150
|
+
}
|
|
151
|
+
/** @deprecated Use getCachedScanRootOverrides(). */
|
|
152
|
+
function getCachedExtraScanRoots() {
|
|
153
|
+
return getCachedScanRootOverrides().extraScanRoots;
|
|
154
|
+
}
|
package/dist/version.json
CHANGED