zeuslock-dlp-cli 0.2.1 → 0.2.2
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/LICENSE +72 -0
- package/README.md +12 -12
- package/package.json +22 -5
- package/src/commands/deploy.js +106 -21
- package/src/commands/incidents.js +24 -8
- package/src/lib/api-client.js +33 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
ZeusLock CLI — End User Licence Agreement
|
|
2
|
+
Copyright (c) 2026 ZeusLock. All rights reserved.
|
|
3
|
+
|
|
4
|
+
This software (the "Software") is proprietary to ZeusLock and is licensed, not
|
|
5
|
+
sold. By downloading, installing or using the Software you agree to the terms
|
|
6
|
+
below. If you do not agree, do not install or use it.
|
|
7
|
+
|
|
8
|
+
1. Grant of licence
|
|
9
|
+
|
|
10
|
+
ZeusLock grants you a non-exclusive, non-transferable, revocable, worldwide
|
|
11
|
+
licence to install and use the Software, in object and source form as
|
|
12
|
+
distributed, solely to access and administer a ZeusLock service for which you
|
|
13
|
+
hold a valid subscription or evaluation entitlement.
|
|
14
|
+
|
|
15
|
+
You may install the Software on any number of machines you or your
|
|
16
|
+
organisation control, and use it in automated systems such as CI pipelines,
|
|
17
|
+
for that purpose.
|
|
18
|
+
|
|
19
|
+
2. Restrictions
|
|
20
|
+
|
|
21
|
+
Except where the applicable law of your jurisdiction grants a right that
|
|
22
|
+
cannot lawfully be waived, you may not:
|
|
23
|
+
|
|
24
|
+
a. distribute, sublicense, sell, rent, lease or host the Software, or make it
|
|
25
|
+
available to any third party as part of a service offering;
|
|
26
|
+
b. modify or create derivative works of the Software, other than local
|
|
27
|
+
configuration and scripting that invokes it as published;
|
|
28
|
+
c. remove or obscure any copyright, trademark or other proprietary notice;
|
|
29
|
+
d. use the Software to access a ZeusLock service you are not authorised to
|
|
30
|
+
access, or to circumvent any licensing, quota or entitlement control.
|
|
31
|
+
|
|
32
|
+
3. Third-party components
|
|
33
|
+
|
|
34
|
+
The Software depends on separately licensed open-source packages, which are
|
|
35
|
+
installed from their own registries and remain governed by their own licence
|
|
36
|
+
terms. Nothing in this Agreement limits any right you have under those terms.
|
|
37
|
+
|
|
38
|
+
4. Ownership
|
|
39
|
+
|
|
40
|
+
ZeusLock and its licensors retain all right, title and interest in and to the
|
|
41
|
+
Software, including all intellectual property rights. No rights are granted
|
|
42
|
+
except as expressly stated in this Agreement.
|
|
43
|
+
|
|
44
|
+
5. Term and termination
|
|
45
|
+
|
|
46
|
+
This licence takes effect on first use and continues until terminated. It
|
|
47
|
+
terminates automatically if you breach any term, or when your entitlement to
|
|
48
|
+
the corresponding ZeusLock service ends. On termination you must stop using
|
|
49
|
+
the Software and remove all copies under your control. Sections 4, 6, 7 and 8
|
|
50
|
+
survive termination.
|
|
51
|
+
|
|
52
|
+
6. No warranty
|
|
53
|
+
|
|
54
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
55
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
56
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
57
|
+
|
|
58
|
+
7. Limitation of liability
|
|
59
|
+
|
|
60
|
+
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL ZEUSLOCK
|
|
61
|
+
OR ITS LICENSORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
62
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
63
|
+
CONNECTION WITH THE SOFTWARE OR ITS USE. Nothing in this Agreement excludes
|
|
64
|
+
or limits liability that cannot lawfully be excluded or limited.
|
|
65
|
+
|
|
66
|
+
8. Precedence
|
|
67
|
+
|
|
68
|
+
If you or your organisation have a signed written agreement with ZeusLock
|
|
69
|
+
covering the Software, that agreement prevails over this one to the extent of
|
|
70
|
+
any conflict.
|
|
71
|
+
|
|
72
|
+
Contact: https://zeuslock.ai
|
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ npm start -- auth api-key set zl_your_org_api_key
|
|
|
55
55
|
npm start -- scan ./README.md --json
|
|
56
56
|
npm start -- anonymize ./dataset.txt --output dataset.safe.txt
|
|
57
57
|
npm start -- hook install --force
|
|
58
|
-
npm start -- deploy
|
|
58
|
+
npm start -- deploy generate --api-key zl_your_key_here --output zeuslock-deploy
|
|
59
59
|
npm start -- deploy agent-verify --hostname CLIENT01
|
|
60
60
|
npm start -- agents list
|
|
61
61
|
npm start -- agents list --status offline --search linux
|
|
@@ -96,7 +96,7 @@ zeuslock scan ./README.md
|
|
|
96
96
|
zeuslock scan --stdin
|
|
97
97
|
zeuslock anonymize ./dataset.txt --output dataset.safe.txt
|
|
98
98
|
zeuslock hook install
|
|
99
|
-
zeuslock deploy
|
|
99
|
+
zeuslock deploy generate --api-key zl_your_key_here --output zeuslock-deploy
|
|
100
100
|
zeuslock deploy agent-verify --hostname CLIENT01
|
|
101
101
|
zeuslock agents list --status online
|
|
102
102
|
zeuslock agents revoke <agent_id> --yes
|
|
@@ -252,20 +252,20 @@ Deployment commands mirror only the dashboard Extension page and Agents deployme
|
|
|
252
252
|
|
|
253
253
|
| Command | Purpose |
|
|
254
254
|
| --- | --- |
|
|
255
|
-
| `zeuslock deploy
|
|
255
|
+
| `zeuslock deploy generate` | Generate enrollment files: extension managed-configuration policies and desktop-agent config. Alias: `gpo-config`. |
|
|
256
256
|
| `zeuslock deploy agent-verify` | Verify installed/enrolled/reachable state from `GET /api/agents`. |
|
|
257
257
|
|
|
258
258
|
Supported filters and values:
|
|
259
259
|
|
|
260
260
|
| Command | Option | Values |
|
|
261
261
|
| --- | --- | --- |
|
|
262
|
-
| `deploy
|
|
263
|
-
| `deploy
|
|
264
|
-
| `deploy
|
|
265
|
-
| `deploy
|
|
266
|
-
| `deploy
|
|
267
|
-
| `deploy
|
|
268
|
-
| `deploy
|
|
262
|
+
| `deploy generate` | `--artifact` | `all`, `extension`, `agent`; default `all` |
|
|
263
|
+
| `deploy generate` | `--browser` | `all`, `chrome`, `edge`; default `all`; applies to extension artifacts |
|
|
264
|
+
| `deploy generate` | `--output` | Output directory; default `zeuslock-deploy` |
|
|
265
|
+
| `deploy generate` | `--api-key` | Full organization API key to embed in generated files |
|
|
266
|
+
| `deploy generate` | `--key-id` | Existing key id from `zeuslock keys list --show-secret`; requires a decryptable key from `GET /api/keys` |
|
|
267
|
+
| `deploy generate` | `--create-key-name` | Creates a new key with `POST /api/keys` and embeds the returned full key |
|
|
268
|
+
| `deploy generate` | `--json` | Prints generated artifact metadata without secret values |
|
|
269
269
|
| `deploy agent-verify` | `--agent-id` | Exact agent id from `zeuslock agents list` |
|
|
270
270
|
| `deploy agent-verify` | `--hostname` | Exact hostname, case-insensitive |
|
|
271
271
|
| `deploy agent-verify` | `--platform` | `windows`, `macos`, `linux` |
|
|
@@ -411,12 +411,12 @@ Supported filters and values:
|
|
|
411
411
|
| Command | Option | Values |
|
|
412
412
|
| --- | --- | --- |
|
|
413
413
|
| `incidents list` | `--days` | `7`, `30`, `90`; `90` requires Business or Enterprise |
|
|
414
|
-
| `incidents list` | `--severity` | `critical`, `warning`; `
|
|
414
|
+
| `incidents list` | `--severity` | `critical`, `warning`; policy outcome — `critical` = blocked, `warning` = everything else |
|
|
415
415
|
| `incidents list` | `--search` | Any text; searches user email, URL, and finding type |
|
|
416
416
|
| `incidents list` | `--jailbreak` | Only incidents with a `jailbreak_attempt` finding |
|
|
417
417
|
| `incidents stats` | `--days` | `1`, `7`, `30`, `365`; `365` requires Business or Enterprise |
|
|
418
418
|
| `incidents export` | `--days` | `7`, `30`, `90`; default `30`; export requires Business or Enterprise |
|
|
419
|
-
| `incidents export` | `--severity` | `critical`, `warning`; `
|
|
419
|
+
| `incidents export` | `--severity` | `critical`, `warning`; policy outcome — `critical` = blocked, `warning` = everything else |
|
|
420
420
|
| `incidents export` | `--search` | Any text; searches user email, URL, and finding type |
|
|
421
421
|
| `incidents export` | `--jailbreak` | Only incidents with a `jailbreak_attempt` finding |
|
|
422
422
|
| `incidents export` | `--format` | `csv`, `json`; default `csv` |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zeuslock-dlp-cli",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "Terminal client for ZeusLock DLP — scan files and prompts for sensitive data, block risky commits, and manage policies, agents, incidents and SIEM events.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"zeuslock": "bin/zeuslock.js"
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"bin",
|
|
11
11
|
"src",
|
|
12
|
-
"README.md"
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
13
14
|
],
|
|
14
15
|
"scripts": {
|
|
15
16
|
"start": "node ./bin/zeuslock.js",
|
|
@@ -22,10 +23,26 @@
|
|
|
22
23
|
},
|
|
23
24
|
"keywords": [
|
|
24
25
|
"zeuslock",
|
|
26
|
+
"dlp",
|
|
27
|
+
"data-loss-prevention",
|
|
25
28
|
"cli",
|
|
26
|
-
"security"
|
|
29
|
+
"security",
|
|
30
|
+
"ai-security",
|
|
31
|
+
"shadow-ai",
|
|
32
|
+
"siem",
|
|
33
|
+
"ocsf",
|
|
34
|
+
"pre-commit",
|
|
35
|
+
"compliance"
|
|
27
36
|
],
|
|
28
|
-
"
|
|
37
|
+
"author": "ZeusLock (https://zeuslock.ai)",
|
|
38
|
+
"homepage": "https://zeuslock.ai/en/docs/getting-started/zeuslock-cli/",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://zeuslock.ai/en/docs/getting-started/zeuslock-cli/"
|
|
41
|
+
},
|
|
42
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
29
46
|
"dependencies": {
|
|
30
47
|
"commander": "^15.0.0",
|
|
31
48
|
"yaml": "^2.9.0"
|
package/src/commands/deploy.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
1
|
+
import { chmod, mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
|
|
@@ -18,12 +18,27 @@ const EXTENSION_ID = "hgooghpcnalhpjbemnnmdoabfjhchoip";
|
|
|
18
18
|
const EXTENSION_UPDATE_URL = "https://clients2.google.com/service/update2/crx";
|
|
19
19
|
const AGENT_ONLINE_WINDOW_MS = 5 * 60 * 1000;
|
|
20
20
|
|
|
21
|
+
// Chromium reads an extension's managed configuration from a policy tree that is
|
|
22
|
+
// SEPARATE from ExtensionSettings — ExtensionSettings only controls install and
|
|
23
|
+
// host permissions, and silently drops any key it does not recognise. Managed
|
|
24
|
+
// values live under `<browser policy root>\3rdparty\extensions\<id>\policy`.
|
|
25
|
+
const EXTENSION_MANAGED_POLICY_ROOT = {
|
|
26
|
+
chrome: "HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Google\\Chrome",
|
|
27
|
+
edge: "HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Microsoft\\Edge"
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Property names the extension actually reads. These must match
|
|
31
|
+
// `managed_schema.json` in zeuslock-extension exactly: Chromium validates
|
|
32
|
+
// managed storage against that schema and discards anything not declared, and
|
|
33
|
+
// background.js gates on `managed.apiUrl && managed.apiKey` (camelCase).
|
|
34
|
+
const EXTENSION_MANAGED_KEYS = ["apiUrl", "apiKey", "orgName", "enabled", "showNotifications"];
|
|
35
|
+
|
|
21
36
|
export function createDeployCommand({
|
|
22
37
|
stdout = process.stdout,
|
|
23
38
|
env = process.env
|
|
24
39
|
} = {}) {
|
|
25
40
|
const command = new Command("deploy")
|
|
26
|
-
.description("Generate and verify ZeusLock
|
|
41
|
+
.description("Generate and verify ZeusLock enrollment files");
|
|
27
42
|
|
|
28
43
|
command.addCommand(createGpoConfigCommand({ stdout, env }));
|
|
29
44
|
command.addCommand(createAgentVerifyCommand({ stdout, env }));
|
|
@@ -32,8 +47,13 @@ export function createDeployCommand({
|
|
|
32
47
|
}
|
|
33
48
|
|
|
34
49
|
function createGpoConfigCommand({ stdout, env }) {
|
|
35
|
-
|
|
36
|
-
|
|
50
|
+
// `generate` is the documented name: this command writes local enrollment
|
|
51
|
+
// files for any rollout channel (GPO, Intune, Jamf, MDM), not GPO alone, and
|
|
52
|
+
// it never touches Active Directory. `gpo-config` stays as an alias so
|
|
53
|
+
// existing scripts and the published docs keep working.
|
|
54
|
+
return new Command("generate")
|
|
55
|
+
.alias("gpo-config")
|
|
56
|
+
.description("Generate managed-configuration files for mass deployment (GPO, Intune, Jamf, MDM)")
|
|
37
57
|
.option("--artifact <set>", "artifact set: all, extension, or agent", "all")
|
|
38
58
|
.option("--browser <browser>", "extension browser policy: chrome, edge, or all", "all")
|
|
39
59
|
.option("--output <dir>", "output directory", "zeuslock-deploy")
|
|
@@ -65,7 +85,8 @@ function createGpoConfigCommand({ stdout, env }) {
|
|
|
65
85
|
outputDir,
|
|
66
86
|
browser,
|
|
67
87
|
apiUrl,
|
|
68
|
-
apiKey: selectedKey.api_key
|
|
88
|
+
apiKey: selectedKey.api_key,
|
|
89
|
+
orgName: org?.name || ""
|
|
69
90
|
}));
|
|
70
91
|
}
|
|
71
92
|
|
|
@@ -312,7 +333,7 @@ function formatMaskedPrefix(prefix) {
|
|
|
312
333
|
return prefix ? `${prefix}***...****` : "";
|
|
313
334
|
}
|
|
314
335
|
|
|
315
|
-
async function writeExtensionArtifacts({ outputDir, browser, apiUrl, apiKey }) {
|
|
336
|
+
async function writeExtensionArtifacts({ outputDir, browser, apiUrl, apiKey, orgName }) {
|
|
316
337
|
const artifacts = [];
|
|
317
338
|
const browsers = browser === "all" ? ["chrome", "edge"] : [browser];
|
|
318
339
|
for (const target of browsers) {
|
|
@@ -325,44 +346,98 @@ async function writeExtensionArtifacts({ outputDir, browser, apiUrl, apiKey }) {
|
|
|
325
346
|
artifacts.push(await writeArtifact(
|
|
326
347
|
outputDir,
|
|
327
348
|
`extension/${target}-extension-settings.json`,
|
|
328
|
-
`${JSON.stringify(buildExtensionSettings({ apiUrl
|
|
349
|
+
`${JSON.stringify(buildExtensionSettings({ apiUrl }), null, 2)}\n`,
|
|
329
350
|
"extension"
|
|
330
351
|
));
|
|
352
|
+
artifacts.push(await writeArtifact(
|
|
353
|
+
outputDir,
|
|
354
|
+
`extension/${target}-extension-managed-policy.reg`,
|
|
355
|
+
buildExtensionManagedRegistry({ browser: target, apiUrl, apiKey, orgName }),
|
|
356
|
+
"extension",
|
|
357
|
+
{ secret: true }
|
|
358
|
+
));
|
|
359
|
+
artifacts.push(await writeArtifact(
|
|
360
|
+
outputDir,
|
|
361
|
+
`extension/${target}-extension-managed-config.json`,
|
|
362
|
+
`${JSON.stringify(buildExtensionManagedConfig({ apiUrl, apiKey, orgName }), null, 2)}\n`,
|
|
363
|
+
"extension",
|
|
364
|
+
{ secret: true }
|
|
365
|
+
));
|
|
331
366
|
}
|
|
332
367
|
return artifacts;
|
|
333
368
|
}
|
|
334
369
|
|
|
335
|
-
|
|
370
|
+
/**
|
|
371
|
+
* Chromium `ExtensionSettings` policy value — install and host permissions only.
|
|
372
|
+
*
|
|
373
|
+
* This deliberately carries NO credentials. An earlier version nested a
|
|
374
|
+
* `configuration` block here; `configuration` is not a recognised
|
|
375
|
+
* ExtensionSettings key, so Chromium dropped it without warning and the
|
|
376
|
+
* extension fell back to per-user setup — the exact thing this command exists to
|
|
377
|
+
* avoid. Managed values go through buildExtensionManagedConfig() instead.
|
|
378
|
+
*/
|
|
379
|
+
function buildExtensionSettings({ apiUrl }) {
|
|
336
380
|
const apiHost = new URL(apiUrl).host;
|
|
337
381
|
return {
|
|
338
382
|
[EXTENSION_ID]: {
|
|
339
383
|
installation_mode: "force_installed",
|
|
384
|
+
update_url: EXTENSION_UPDATE_URL,
|
|
340
385
|
runtime_allowed_hosts: [`*://${apiHost}`],
|
|
341
|
-
runtime_blocked_hosts: []
|
|
342
|
-
configuration: {
|
|
343
|
-
api_url: apiUrl,
|
|
344
|
-
api_key: apiKey,
|
|
345
|
-
monitor_clipboard: true,
|
|
346
|
-
monitor_ai_apps: true,
|
|
347
|
-
show_notifications: true
|
|
348
|
-
}
|
|
386
|
+
runtime_blocked_hosts: []
|
|
349
387
|
}
|
|
350
388
|
};
|
|
351
389
|
}
|
|
352
390
|
|
|
391
|
+
/** Managed-storage values the extension reads via `chrome.storage.managed`. */
|
|
392
|
+
function buildExtensionManagedConfig({ apiUrl, apiKey, orgName }) {
|
|
393
|
+
return {
|
|
394
|
+
apiUrl,
|
|
395
|
+
apiKey,
|
|
396
|
+
orgName: orgName || "",
|
|
397
|
+
enabled: true,
|
|
398
|
+
showNotifications: true
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* The same managed configuration as a Windows `.reg` file, for GPO rollout.
|
|
404
|
+
* Booleans must be REG_DWORD; Chromium rejects "true"/"false" strings.
|
|
405
|
+
*/
|
|
406
|
+
function buildExtensionManagedRegistry({ browser, apiUrl, apiKey, orgName }) {
|
|
407
|
+
const config = buildExtensionManagedConfig({ apiUrl, apiKey, orgName });
|
|
408
|
+
const lines = [
|
|
409
|
+
"Windows Registry Editor Version 5.00",
|
|
410
|
+
"",
|
|
411
|
+
`; ZeusLock extension managed configuration (${browser}).`,
|
|
412
|
+
"; Read by the extension through chrome.storage.managed.",
|
|
413
|
+
"",
|
|
414
|
+
`[${EXTENSION_MANAGED_POLICY_ROOT[browser]}\\3rdparty\\extensions\\${EXTENSION_ID}\\policy]`
|
|
415
|
+
];
|
|
416
|
+
for (const key of EXTENSION_MANAGED_KEYS) {
|
|
417
|
+
const value = config[key];
|
|
418
|
+
lines.push(typeof value === "boolean"
|
|
419
|
+
? `"${key}"=dword:${value ? "00000001" : "00000000"}`
|
|
420
|
+
: `"${key}"="${escapeRegString(String(value ?? ""))}"`);
|
|
421
|
+
}
|
|
422
|
+
lines.push("");
|
|
423
|
+
return lines.join("\r\n");
|
|
424
|
+
}
|
|
425
|
+
|
|
353
426
|
async function writeAgentArtifacts({ outputDir, apiUrl, apiKey }) {
|
|
354
427
|
return [
|
|
355
428
|
await writeArtifact(
|
|
356
429
|
outputDir,
|
|
357
430
|
"agent/windows-agent-policy.reg",
|
|
358
431
|
buildWindowsAgentRegistry({ apiUrl, apiKey }),
|
|
359
|
-
"agent"
|
|
432
|
+
"agent",
|
|
433
|
+
{ secret: true }
|
|
360
434
|
),
|
|
361
435
|
await writeArtifact(
|
|
362
436
|
outputDir,
|
|
363
437
|
"agent/agent-config.json",
|
|
364
438
|
`${JSON.stringify(buildAgentConfig({ apiUrl, apiKey }), null, 2)}\n`,
|
|
365
|
-
"agent"
|
|
439
|
+
"agent",
|
|
440
|
+
{ secret: true }
|
|
366
441
|
)
|
|
367
442
|
];
|
|
368
443
|
}
|
|
@@ -391,13 +466,23 @@ function escapeRegString(value) {
|
|
|
391
466
|
return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
392
467
|
}
|
|
393
468
|
|
|
394
|
-
async function writeArtifact(outputDir, relativePath, content, type) {
|
|
469
|
+
async function writeArtifact(outputDir, relativePath, content, type, { secret = false } = {}) {
|
|
395
470
|
const fullPath = path.join(outputDir, relativePath);
|
|
396
471
|
await mkdir(path.dirname(fullPath), { recursive: true });
|
|
397
|
-
|
|
472
|
+
// Artifacts carrying the organization API key get the same 0600 treatment as
|
|
473
|
+
// the CLI's own api-key.json — writing a plaintext fleet credential
|
|
474
|
+
// world-readable would undo that hardening. `mode` is a no-op on Windows.
|
|
475
|
+
await writeFile(fullPath, content, { encoding: "utf8", mode: secret ? 0o600 : 0o644 });
|
|
476
|
+
if (secret) {
|
|
477
|
+
// writeFile's `mode` only applies when the file is created, and this command
|
|
478
|
+
// overwrites on re-run — chmod explicitly so a file left over from an older
|
|
479
|
+
// build does not keep 0644. Best-effort: unsupported on Windows.
|
|
480
|
+
await chmod(fullPath, 0o600).catch(() => {});
|
|
481
|
+
}
|
|
398
482
|
return {
|
|
399
483
|
type,
|
|
400
|
-
path: fullPath
|
|
484
|
+
path: fullPath,
|
|
485
|
+
...(secret ? { contains_secret: true } : {})
|
|
401
486
|
};
|
|
402
487
|
}
|
|
403
488
|
|
|
@@ -209,13 +209,7 @@ function applyIncidentFilters(incidents, { severity = null, search = "", jailbre
|
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
if (severity) {
|
|
212
|
-
filtered = filtered.filter((incident) =>
|
|
213
|
-
const riskLevel = String(incident.risk_level || incident.severity || "").toLowerCase();
|
|
214
|
-
if (severity === "warning") {
|
|
215
|
-
return ["high", "medium", "low"].includes(riskLevel);
|
|
216
|
-
}
|
|
217
|
-
return riskLevel === severity;
|
|
218
|
-
});
|
|
212
|
+
filtered = filtered.filter((incident) => outcomeSeverity(incident) === severity);
|
|
219
213
|
}
|
|
220
214
|
|
|
221
215
|
const query = String(search || "").trim().toLowerCase();
|
|
@@ -230,6 +224,24 @@ function applyIncidentFilters(incidents, { severity = null, search = "", jailbre
|
|
|
230
224
|
return filtered;
|
|
231
225
|
}
|
|
232
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Severity as the dashboard shows it: the POLICY OUTCOME decides, not the
|
|
229
|
+
* detector's risk score. A credit-card hit under an `alert` policy is a
|
|
230
|
+
* warning; the same hit under a `block` policy is critical.
|
|
231
|
+
*
|
|
232
|
+
* Kept byte-for-byte equivalent to `outcomeSeverity()` in the dashboard's
|
|
233
|
+
* IncidentsPage.tsx. The CLI used to read `risk_level` here, which disagreed
|
|
234
|
+
* with the dashboard on 44% of rows (alert-mode findings still carry
|
|
235
|
+
* risk_level "critical"). Change both or neither.
|
|
236
|
+
*/
|
|
237
|
+
function outcomeSeverity(incident) {
|
|
238
|
+
const action = String(incident?.action || "").toLowerCase();
|
|
239
|
+
if (action) {
|
|
240
|
+
return action === "blocked" || action === "block" ? "critical" : "warning";
|
|
241
|
+
}
|
|
242
|
+
return incident?.blocked === true ? "critical" : "warning";
|
|
243
|
+
}
|
|
244
|
+
|
|
233
245
|
function isDashboardVisibleIncident(incident) {
|
|
234
246
|
const hasFindings = Array.isArray(incident.findings) && incident.findings.length > 0;
|
|
235
247
|
const isBlocked = incident.blocked === true;
|
|
@@ -250,7 +262,7 @@ function getFindingTypes(incident) {
|
|
|
250
262
|
function formatIncidentTable(incidents) {
|
|
251
263
|
const rows = incidents.map((incident) => ({
|
|
252
264
|
date: formatDate(incident.created_at),
|
|
253
|
-
severity: incident
|
|
265
|
+
severity: outcomeSeverity(incident),
|
|
254
266
|
type: formatFindingTypes(incident),
|
|
255
267
|
user: incident.user_email || "-",
|
|
256
268
|
platform: formatPlatform(incident),
|
|
@@ -365,6 +377,10 @@ function formatIncidentsCsv(incidents) {
|
|
|
365
377
|
const columns = [
|
|
366
378
|
["incident_id", (incident) => incident.incident_id || incident.id || ""],
|
|
367
379
|
["created_at", (incident) => incident.created_at || ""],
|
|
380
|
+
// `severity` is the dashboard-visible value (policy outcome); `risk_level`
|
|
381
|
+
// is kept alongside it as the raw detector score so nothing is lost for
|
|
382
|
+
// anyone already parsing that column.
|
|
383
|
+
["severity", (incident) => outcomeSeverity(incident)],
|
|
368
384
|
["risk_level", (incident) => incident.risk_level || incident.severity || ""],
|
|
369
385
|
["type", (incident) => formatFindingTypes(incident)],
|
|
370
386
|
["user_email", (incident) => incident.user_email || ""],
|
package/src/lib/api-client.js
CHANGED
|
@@ -292,9 +292,9 @@ async function fetchJson(url, request) {
|
|
|
292
292
|
|
|
293
293
|
if (!response.ok) {
|
|
294
294
|
const message =
|
|
295
|
-
data?.error ||
|
|
296
|
-
data?.detail ||
|
|
297
|
-
data?.message ||
|
|
295
|
+
errorText(data?.error) ||
|
|
296
|
+
errorText(data?.detail) ||
|
|
297
|
+
errorText(data?.message) ||
|
|
298
298
|
responseText ||
|
|
299
299
|
`Request failed with status ${response.status}`;
|
|
300
300
|
throw new ApiError(message, { status: response.status, data, responseText });
|
|
@@ -303,6 +303,36 @@ async function fetchJson(url, request) {
|
|
|
303
303
|
return data ?? {};
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Coerce one backend error field into displayable text.
|
|
308
|
+
*
|
|
309
|
+
* Most endpoints return `detail` as a plain string, but the policy, group and
|
|
310
|
+
* principal routes wrap it (`{"detail": {"error": "Policy not found"}}` — see
|
|
311
|
+
* `policies.py` `_translate`, which passes `detail=exc.to_dict()`). Handing that
|
|
312
|
+
* object straight to `new Error()` printed the literal `[object Object]` and
|
|
313
|
+
* threw away a perfectly good message, so unwrap the common shapes first.
|
|
314
|
+
*/
|
|
315
|
+
function errorText(value) {
|
|
316
|
+
if (typeof value === "string") {
|
|
317
|
+
return value.trim() || null;
|
|
318
|
+
}
|
|
319
|
+
if (Array.isArray(value)) {
|
|
320
|
+
// FastAPI request-validation errors arrive as a list of {loc, msg, type}.
|
|
321
|
+
const parts = value.map((entry) => errorText(entry)).filter(Boolean);
|
|
322
|
+
return parts.length ? parts.join("; ") : null;
|
|
323
|
+
}
|
|
324
|
+
if (value && typeof value === "object") {
|
|
325
|
+
return (
|
|
326
|
+
errorText(value.error) ||
|
|
327
|
+
errorText(value.message) ||
|
|
328
|
+
errorText(value.msg) ||
|
|
329
|
+
errorText(value.detail) ||
|
|
330
|
+
null
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
306
336
|
function ensureTrailingSlash(apiUrl) {
|
|
307
337
|
return apiUrl.endsWith("/") ? apiUrl : `${apiUrl}/`;
|
|
308
338
|
}
|