zeuslock-dlp-cli 0.2.0
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/README.md +570 -0
- package/bin/zeuslock.js +9 -0
- package/package.json +33 -0
- package/src/cli.js +78 -0
- package/src/commands/agents.js +211 -0
- package/src/commands/anonymize.js +83 -0
- package/src/commands/auth.js +337 -0
- package/src/commands/deploy.js +515 -0
- package/src/commands/extensions.js +73 -0
- package/src/commands/hook.js +221 -0
- package/src/commands/incidents.js +436 -0
- package/src/commands/keys.js +211 -0
- package/src/commands/mcp.js +322 -0
- package/src/commands/rules.js +432 -0
- package/src/commands/scan.js +178 -0
- package/src/commands/shadow-ai.js +255 -0
- package/src/commands/siem.js +241 -0
- package/src/commands/status.js +170 -0
- package/src/commands/tokens.js +293 -0
- package/src/commands/users.js +255 -0
- package/src/commands/whoami.js +43 -0
- package/src/lib/api-client.js +308 -0
- package/src/lib/api-key-store.js +84 -0
- package/src/lib/auth-store.js +123 -0
- package/src/lib/cli-token.js +22 -0
- package/src/lib/command-token.js +15 -0
- package/src/lib/config.js +27 -0
- package/src/lib/dlp-scan.js +146 -0
- package/src/lib/package-info.js +11 -0
- package/src/lib/prompt.js +55 -0
- package/src/lib/siem-cursors.js +64 -0
- package/src/lib/table.js +30 -0
- package/src/lib/time.js +33 -0
- package/src/lib/version.js +24 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { chmod, mkdir, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
analyzeDlpBatch,
|
|
10
|
+
formatDlpSummary,
|
|
11
|
+
parseFailOn,
|
|
12
|
+
summarizeDlpResponses
|
|
13
|
+
} from "../lib/dlp-scan.js";
|
|
14
|
+
|
|
15
|
+
const execFileAsync = promisify(execFile);
|
|
16
|
+
const BATCH_SIZE = 5;
|
|
17
|
+
const GIT_MAX_BUFFER = 16 * 1024 * 1024;
|
|
18
|
+
|
|
19
|
+
export function createHookCommand({
|
|
20
|
+
stdout = process.stdout,
|
|
21
|
+
env = process.env
|
|
22
|
+
} = {}) {
|
|
23
|
+
const command = new Command("hook")
|
|
24
|
+
.description("Install and run Git DLP hooks");
|
|
25
|
+
|
|
26
|
+
command.addCommand(createHookInstallCommand({ stdout, env }));
|
|
27
|
+
command.addCommand(createHookRunCommand({ stdout, env }));
|
|
28
|
+
|
|
29
|
+
return command;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createHookInstallCommand({ stdout, env }) {
|
|
33
|
+
return new Command("install")
|
|
34
|
+
.description("Install a Git pre-commit hook that runs ZeusLock DLP scan")
|
|
35
|
+
.option("--repo <path>", "Git repository path", process.cwd())
|
|
36
|
+
.option("--fail-on <decision>", "block commit on alert or block", "alert")
|
|
37
|
+
.option("--force", "replace an existing pre-commit hook")
|
|
38
|
+
.option("--json", "print machine-readable output")
|
|
39
|
+
.action(async (options) => {
|
|
40
|
+
const failOn = parseFailOn(options.failOn);
|
|
41
|
+
if (failOn === "never") {
|
|
42
|
+
throw new Error("--fail-on never is not supported for hook install.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const repoRoot = await gitText(["rev-parse", "--show-toplevel"], options.repo, env);
|
|
46
|
+
const gitHookPath = await gitText(["rev-parse", "--git-path", "hooks/pre-commit"], repoRoot, env);
|
|
47
|
+
const hookPath = path.resolve(repoRoot, gitHookPath);
|
|
48
|
+
const exists = await pathExists(hookPath);
|
|
49
|
+
if (exists && !options.force) {
|
|
50
|
+
throw new Error(`Pre-commit hook already exists at ${hookPath}. Re-run with --force to replace it.`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await mkdir(path.dirname(hookPath), { recursive: true });
|
|
54
|
+
await writeFile(hookPath, buildHookScript(failOn), "utf8");
|
|
55
|
+
await chmod(hookPath, 0o755);
|
|
56
|
+
|
|
57
|
+
const output = {
|
|
58
|
+
installed: true,
|
|
59
|
+
repo: repoRoot,
|
|
60
|
+
hook: hookPath,
|
|
61
|
+
failOn,
|
|
62
|
+
replaced: exists
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (options.json) {
|
|
66
|
+
stdout.write(`${JSON.stringify(output)}\n`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
stdout.write(`Installed ZeusLock pre-commit hook at ${hookPath}\n`);
|
|
71
|
+
stdout.write(`Commit block threshold: ${failOn}\n`);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function createHookRunCommand({ stdout, env }) {
|
|
76
|
+
return new Command("run")
|
|
77
|
+
.description("Run ZeusLock DLP scan against staged Git files")
|
|
78
|
+
.option("--repo <path>", "Git repository path", process.cwd())
|
|
79
|
+
.option("--api-key <key>", "organization API key; defaults to saved key or ZEUSLOCK_API_KEY")
|
|
80
|
+
.option("--fail-on <decision>", "exit nonzero on alert, block, or never", "alert")
|
|
81
|
+
.option("--user-email <email>", "user email label sent to backend")
|
|
82
|
+
.option("--json", "print machine-readable output")
|
|
83
|
+
.action(async (options) => {
|
|
84
|
+
const failOn = parseFailOn(options.failOn);
|
|
85
|
+
const repoRoot = await gitText(["rev-parse", "--show-toplevel"], options.repo, env);
|
|
86
|
+
const stagedFiles = await listStagedFiles(repoRoot, env);
|
|
87
|
+
|
|
88
|
+
if (!stagedFiles.length) {
|
|
89
|
+
const output = {
|
|
90
|
+
scanned: { files: 0, text: false },
|
|
91
|
+
decision: "allow",
|
|
92
|
+
failed: false,
|
|
93
|
+
failOn,
|
|
94
|
+
responses: []
|
|
95
|
+
};
|
|
96
|
+
stdout.write(options.json ? `${JSON.stringify(output)}\n` : "No staged files to scan.\n");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const files = [];
|
|
101
|
+
for (const file of stagedFiles) {
|
|
102
|
+
files.push({
|
|
103
|
+
filename: file,
|
|
104
|
+
contentType: "application/octet-stream",
|
|
105
|
+
data: await gitBuffer(["show", `:${file}`], repoRoot, env)
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const responses = [];
|
|
110
|
+
for (const batch of chunk(files, BATCH_SIZE)) {
|
|
111
|
+
responses.push(await analyzeDlpBatch({
|
|
112
|
+
files: batch,
|
|
113
|
+
metadata: {
|
|
114
|
+
source: "git_pre_commit",
|
|
115
|
+
platform: "git",
|
|
116
|
+
hostname: os.hostname(),
|
|
117
|
+
path: "/git/pre-commit",
|
|
118
|
+
method: "PRE_COMMIT",
|
|
119
|
+
userEmail: options.userEmail || null
|
|
120
|
+
},
|
|
121
|
+
apiKey: options.apiKey,
|
|
122
|
+
env
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const summary = summarizeDlpResponses(responses, {
|
|
127
|
+
failOn,
|
|
128
|
+
includeSensitive: false
|
|
129
|
+
});
|
|
130
|
+
const output = {
|
|
131
|
+
...summary,
|
|
132
|
+
repo: repoRoot,
|
|
133
|
+
scanned: {
|
|
134
|
+
files: files.length,
|
|
135
|
+
text: false
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
if (options.json) {
|
|
140
|
+
stdout.write(`${JSON.stringify(output)}\n`);
|
|
141
|
+
} else {
|
|
142
|
+
stdout.write(formatDlpSummary(summary, { scannedFiles: files.length }));
|
|
143
|
+
if (summary.failed) {
|
|
144
|
+
stdout.write("Commit blocked by ZeusLock DLP policy.\n");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (summary.failed) {
|
|
149
|
+
process.exitCode = 1;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function buildHookScript(failOn) {
|
|
155
|
+
return `#!/bin/sh
|
|
156
|
+
# Generated by ZeusLock CLI. Reinstall with: zeuslock hook install --force
|
|
157
|
+
if ! command -v zeuslock >/dev/null 2>&1; then
|
|
158
|
+
echo "ZeusLock pre-commit hook failed: zeuslock command not found in PATH" >&2
|
|
159
|
+
exit 1
|
|
160
|
+
fi
|
|
161
|
+
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
162
|
+
exec zeuslock hook run --repo "$REPO_ROOT" --fail-on ${failOn}
|
|
163
|
+
`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function listStagedFiles(repoRoot, env) {
|
|
167
|
+
const output = await gitBuffer([
|
|
168
|
+
"diff",
|
|
169
|
+
"--cached",
|
|
170
|
+
"--name-only",
|
|
171
|
+
"--diff-filter=ACMR",
|
|
172
|
+
"-z"
|
|
173
|
+
], repoRoot, env);
|
|
174
|
+
return output
|
|
175
|
+
.toString("utf8")
|
|
176
|
+
.split("\0")
|
|
177
|
+
.map((entry) => entry.trim())
|
|
178
|
+
.filter(Boolean);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function gitText(args, cwd, env) {
|
|
182
|
+
const output = await gitBuffer(args, cwd, env);
|
|
183
|
+
return output.toString("utf8").trim();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function gitBuffer(args, cwd, env) {
|
|
187
|
+
try {
|
|
188
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
189
|
+
cwd: path.resolve(cwd),
|
|
190
|
+
env,
|
|
191
|
+
encoding: "buffer",
|
|
192
|
+
maxBuffer: GIT_MAX_BUFFER
|
|
193
|
+
});
|
|
194
|
+
return stdout;
|
|
195
|
+
} catch (error) {
|
|
196
|
+
const stderr = Buffer.isBuffer(error?.stderr)
|
|
197
|
+
? error.stderr.toString("utf8").trim()
|
|
198
|
+
: String(error?.stderr || "").trim();
|
|
199
|
+
throw new Error(stderr || error?.message || "Git command failed");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function pathExists(filePath) {
|
|
204
|
+
try {
|
|
205
|
+
await stat(filePath);
|
|
206
|
+
return true;
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (error?.code === "ENOENT") {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function chunk(items, size) {
|
|
216
|
+
const batches = [];
|
|
217
|
+
for (let i = 0; i < items.length; i += size) {
|
|
218
|
+
batches.push(items.slice(i, i + size));
|
|
219
|
+
}
|
|
220
|
+
return batches;
|
|
221
|
+
}
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
|
|
5
|
+
import { requestAuthenticatedJson } from "../lib/api-client.js";
|
|
6
|
+
|
|
7
|
+
const LIST_DAYS = [7, 30, 90];
|
|
8
|
+
const STATS_DAYS = [1, 7, 30, 365];
|
|
9
|
+
const SEVERITIES = ["critical", "warning"];
|
|
10
|
+
const EXPORT_FORMATS = ["csv", "json"];
|
|
11
|
+
const BUSINESS_PLANS = new Set(["business", "enterprise"]);
|
|
12
|
+
|
|
13
|
+
export function createIncidentsCommand({
|
|
14
|
+
stdout = process.stdout,
|
|
15
|
+
env = process.env
|
|
16
|
+
} = {}) {
|
|
17
|
+
const command = new Command("incidents")
|
|
18
|
+
.description("Review and export ZeusLock security incidents");
|
|
19
|
+
|
|
20
|
+
command.addCommand(createListCommand({ stdout, env }));
|
|
21
|
+
command.addCommand(createStatsCommand({ stdout, env }));
|
|
22
|
+
command.addCommand(createExportCommand({ stdout, env }));
|
|
23
|
+
|
|
24
|
+
return command;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createListCommand({ stdout, env }) {
|
|
28
|
+
return new Command("list")
|
|
29
|
+
.description("List recent incidents")
|
|
30
|
+
.option("--days <days>", "incident history window: 7, 30, or 90", "30")
|
|
31
|
+
.option("--severity <level>", "severity filter: critical or warning")
|
|
32
|
+
.option("--search <text>", "search user email, URL, or finding type")
|
|
33
|
+
.option("--jailbreak", "show only jailbreak-attempt incidents")
|
|
34
|
+
.option("--json", "print machine-readable output")
|
|
35
|
+
.action(async (options) => {
|
|
36
|
+
const days = parseAllowedInt(options.days, LIST_DAYS, "--days");
|
|
37
|
+
const severity = parseOptionalChoice(options.severity, SEVERITIES, "--severity");
|
|
38
|
+
|
|
39
|
+
if (days === 90) {
|
|
40
|
+
await requireBusinessPlan({
|
|
41
|
+
env,
|
|
42
|
+
message: "90-day history is only available on Business and Enterprise plans."
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const data = await requestAuthenticatedJson(`/api/incidents?days=${days}`, { env });
|
|
47
|
+
const incidents = applyIncidentFilters(normalizeIncidents(data), {
|
|
48
|
+
severity,
|
|
49
|
+
search: options.search,
|
|
50
|
+
jailbreak: Boolean(options.jailbreak)
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (options.json) {
|
|
54
|
+
stdout.write(`${JSON.stringify(incidents)}\n`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (incidents.length === 0) {
|
|
59
|
+
stdout.write("No incidents found\n");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
stdout.write(formatIncidentTable(incidents));
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createStatsCommand({ stdout, env }) {
|
|
68
|
+
return new Command("stats")
|
|
69
|
+
.description("Show incident statistics")
|
|
70
|
+
.option("--days <days>", "stats window: 1, 7, 30, or 365", "30")
|
|
71
|
+
.option("--json", "print machine-readable output")
|
|
72
|
+
.action(async (options) => {
|
|
73
|
+
const days = parseAllowedInt(options.days, STATS_DAYS, "--days");
|
|
74
|
+
|
|
75
|
+
if (days === 365) {
|
|
76
|
+
await requireBusinessPlan({
|
|
77
|
+
env,
|
|
78
|
+
message: "Yearly history is only available on Business and Enterprise plans."
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const stats = await requestAuthenticatedJson(`/api/stats?days=${days}`, { env });
|
|
83
|
+
|
|
84
|
+
if (options.json) {
|
|
85
|
+
stdout.write(`${JSON.stringify(stats)}\n`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
stdout.write(formatStats(stats, days));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function createExportCommand({ stdout, env }) {
|
|
94
|
+
return new Command("export")
|
|
95
|
+
.description("Export dashboard-filtered incidents")
|
|
96
|
+
.option("--days <days>", "incident history window: 7, 30, or 90", "30")
|
|
97
|
+
.option("--severity <level>", "severity filter: critical or warning")
|
|
98
|
+
.option("--search <text>", "search user email, URL, or finding type")
|
|
99
|
+
.option("--jailbreak", "export only jailbreak-attempt incidents")
|
|
100
|
+
.option("--format <format>", "export format: csv or json", "csv")
|
|
101
|
+
.option("--output <path>", "write export to this path instead of a timestamped file")
|
|
102
|
+
.option("--json", "print machine-readable result metadata")
|
|
103
|
+
.action(async (options) => {
|
|
104
|
+
const days = parseAllowedInt(options.days, LIST_DAYS, "--days");
|
|
105
|
+
const severity = parseOptionalChoice(options.severity, SEVERITIES, "--severity");
|
|
106
|
+
const format = parseOptionalChoice(options.format, EXPORT_FORMATS, "--format") || "csv";
|
|
107
|
+
|
|
108
|
+
await requireBusinessPlan({
|
|
109
|
+
env,
|
|
110
|
+
message: "Incident export is only available on Business and Enterprise plans."
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const data = await requestAuthenticatedJson(`/api/incidents?days=${days}`, { env });
|
|
114
|
+
const incidents = applyIncidentFilters(normalizeIncidents(data), {
|
|
115
|
+
severity,
|
|
116
|
+
search: options.search,
|
|
117
|
+
jailbreak: Boolean(options.jailbreak)
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const content = format === "json"
|
|
121
|
+
? `${JSON.stringify(incidents, null, 2)}\n`
|
|
122
|
+
: formatIncidentsCsv(incidents);
|
|
123
|
+
const outputPath = path.resolve(options.output || `incidents-${Date.now()}.${format}`);
|
|
124
|
+
|
|
125
|
+
if (incidents.length === 0) {
|
|
126
|
+
if (options.json) {
|
|
127
|
+
stdout.write(`${JSON.stringify({
|
|
128
|
+
exported: false,
|
|
129
|
+
format,
|
|
130
|
+
output: null,
|
|
131
|
+
totalCount: 0,
|
|
132
|
+
filters: buildIncidentFilterMetadata({ days, severity, search: options.search, jailbreak: Boolean(options.jailbreak) })
|
|
133
|
+
})}\n`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
stdout.write("No incidents to export\n");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
await writeFile(outputPath, content, "utf8");
|
|
141
|
+
|
|
142
|
+
if (options.json) {
|
|
143
|
+
stdout.write(`${JSON.stringify({
|
|
144
|
+
exported: true,
|
|
145
|
+
format,
|
|
146
|
+
output: outputPath,
|
|
147
|
+
totalCount: incidents.length,
|
|
148
|
+
filters: buildIncidentFilterMetadata({ days, severity, search: options.search, jailbreak: Boolean(options.jailbreak) })
|
|
149
|
+
})}\n`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
stdout.write(`Exported ${incidents.length} incidents to ${outputPath}\n`);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseAllowedInt(value, allowed, optionName) {
|
|
158
|
+
const number = Number(value);
|
|
159
|
+
if (!Number.isInteger(number) || !allowed.includes(number)) {
|
|
160
|
+
throw new Error(`Invalid ${optionName}. Allowed values: ${allowed.join(", ")}.`);
|
|
161
|
+
}
|
|
162
|
+
return number;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseOptionalChoice(value, allowed, optionName) {
|
|
166
|
+
if (value === undefined || value === null || value === "") {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
const normalized = String(value).trim().toLowerCase();
|
|
170
|
+
if (!allowed.includes(normalized)) {
|
|
171
|
+
throw new Error(`Invalid ${optionName}. Allowed values: ${allowed.join(", ")}.`);
|
|
172
|
+
}
|
|
173
|
+
return normalized;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function requireBusinessPlan({ env, message }) {
|
|
177
|
+
const org = await requestAuthenticatedJson("/api/org", { env });
|
|
178
|
+
const plan = String(org?.plan || "free").toLowerCase();
|
|
179
|
+
if (!BUSINESS_PLANS.has(plan)) {
|
|
180
|
+
throw new Error(message);
|
|
181
|
+
}
|
|
182
|
+
return org;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function buildIncidentFilterMetadata({ days, severity = null, search = "", jailbreak = false }) {
|
|
186
|
+
return {
|
|
187
|
+
days,
|
|
188
|
+
severity,
|
|
189
|
+
search: search || null,
|
|
190
|
+
jailbreak
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function normalizeIncidents(data) {
|
|
195
|
+
if (Array.isArray(data)) {
|
|
196
|
+
return data;
|
|
197
|
+
}
|
|
198
|
+
if (Array.isArray(data?.incidents)) {
|
|
199
|
+
return data.incidents;
|
|
200
|
+
}
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function applyIncidentFilters(incidents, { severity = null, search = "", jailbreak = false } = {}) {
|
|
205
|
+
let filtered = incidents.filter(isDashboardVisibleIncident);
|
|
206
|
+
|
|
207
|
+
if (jailbreak) {
|
|
208
|
+
filtered = filtered.filter(isJailbreakIncident);
|
|
209
|
+
}
|
|
210
|
+
|
|
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
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const query = String(search || "").trim().toLowerCase();
|
|
222
|
+
if (query) {
|
|
223
|
+
filtered = filtered.filter((incident) =>
|
|
224
|
+
String(incident.user_email || "").toLowerCase().includes(query) ||
|
|
225
|
+
String(incident.url || "").toLowerCase().includes(query) ||
|
|
226
|
+
getFindingTypes(incident).some((type) => type.toLowerCase().includes(query))
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return filtered;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isDashboardVisibleIncident(incident) {
|
|
234
|
+
const hasFindings = Array.isArray(incident.findings) && incident.findings.length > 0;
|
|
235
|
+
const isBlocked = incident.blocked === true;
|
|
236
|
+
const hasAction = incident.action && incident.action !== "allowed";
|
|
237
|
+
return hasFindings || isBlocked || hasAction;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function isJailbreakIncident(incident) {
|
|
241
|
+
return getFindingTypes(incident).includes("jailbreak_attempt");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function getFindingTypes(incident) {
|
|
245
|
+
return Array.isArray(incident.findings)
|
|
246
|
+
? incident.findings.map((finding) => String(finding?.type || "")).filter(Boolean)
|
|
247
|
+
: [];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function formatIncidentTable(incidents) {
|
|
251
|
+
const rows = incidents.map((incident) => ({
|
|
252
|
+
date: formatDate(incident.created_at),
|
|
253
|
+
severity: incident.risk_level || incident.severity || "-",
|
|
254
|
+
type: formatFindingTypes(incident),
|
|
255
|
+
user: incident.user_email || "-",
|
|
256
|
+
platform: formatPlatform(incident),
|
|
257
|
+
decision: formatDecision(incident),
|
|
258
|
+
source: incident.url || incident.source || "-"
|
|
259
|
+
}));
|
|
260
|
+
|
|
261
|
+
return formatTable(rows, [
|
|
262
|
+
{ key: "date", label: "Date", maxWidth: 24 },
|
|
263
|
+
{ key: "severity", label: "Severity", maxWidth: 10 },
|
|
264
|
+
{ key: "type", label: "Type", maxWidth: 32 },
|
|
265
|
+
{ key: "user", label: "User", maxWidth: 32 },
|
|
266
|
+
{ key: "platform", label: "AI Platform", maxWidth: 12 },
|
|
267
|
+
{ key: "decision", label: "Decision", maxWidth: 10 },
|
|
268
|
+
{ key: "source", label: "Source", maxWidth: 50 }
|
|
269
|
+
]);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function formatFindingTypes(incident) {
|
|
273
|
+
const types = getFindingTypes(incident);
|
|
274
|
+
if (types.length === 0) {
|
|
275
|
+
return "Unknown";
|
|
276
|
+
}
|
|
277
|
+
if (types.length <= 2) {
|
|
278
|
+
return types.join(", ");
|
|
279
|
+
}
|
|
280
|
+
return `${types.slice(0, 2).join(", ")} +${types.length - 2}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function formatPlatform(incident) {
|
|
284
|
+
const platform = incident.platform || inferPlatformFromUrl(incident.url);
|
|
285
|
+
return platform || "-";
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function inferPlatformFromUrl(urlValue) {
|
|
289
|
+
const url = String(urlValue || "").toLowerCase();
|
|
290
|
+
if (!url) return "";
|
|
291
|
+
if (url.includes("chatgpt") || url.includes("openai.com")) return "chatgpt";
|
|
292
|
+
if (url.includes("claude")) return "claude";
|
|
293
|
+
if (url.includes("gemini")) return "gemini";
|
|
294
|
+
if (url.includes("copilot")) return "copilot";
|
|
295
|
+
if (url.includes("grok")) return "grok";
|
|
296
|
+
return "";
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function formatDecision(incident) {
|
|
300
|
+
const action = String(incident.action || "").toLowerCase();
|
|
301
|
+
if (incident.blocked === true || action === "blocked" || action === "block") {
|
|
302
|
+
return "block";
|
|
303
|
+
}
|
|
304
|
+
if (action === "alerted" || action === "alert") {
|
|
305
|
+
return "alert";
|
|
306
|
+
}
|
|
307
|
+
return action || "-";
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function formatStats(stats, days) {
|
|
311
|
+
const lines = [];
|
|
312
|
+
lines.push(`Incidents stats (${days} days)`);
|
|
313
|
+
lines.push(`Total incidents: ${stats?.total_incidents ?? 0}`);
|
|
314
|
+
lines.push(`Prompts captured: ${stats?.prompts_captured ?? 0}`);
|
|
315
|
+
lines.push(`Active agents: ${stats?.active_agents ?? 0}`);
|
|
316
|
+
lines.push("");
|
|
317
|
+
|
|
318
|
+
lines.push("Severity:");
|
|
319
|
+
appendKeyValueLines(lines, stats?.by_risk_level, ["critical", "high", "medium", "low"]);
|
|
320
|
+
lines.push("");
|
|
321
|
+
|
|
322
|
+
lines.push("Detected data types:");
|
|
323
|
+
appendKeyValueLines(lines, stats?.by_type);
|
|
324
|
+
lines.push("");
|
|
325
|
+
|
|
326
|
+
lines.push("Timeline:");
|
|
327
|
+
const timeline = Array.isArray(stats?.daily_incidents) ? stats.daily_incidents : [];
|
|
328
|
+
if (timeline.length === 0) {
|
|
329
|
+
lines.push(" No timeline data");
|
|
330
|
+
} else {
|
|
331
|
+
for (const point of timeline) {
|
|
332
|
+
lines.push(` ${point.date}: ${point.count ?? 0}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
lines.push("");
|
|
336
|
+
|
|
337
|
+
lines.push("AI platform usage:");
|
|
338
|
+
const platforms = Array.isArray(stats?.ai_usage?.platforms) ? stats.ai_usage.platforms : [];
|
|
339
|
+
if (platforms.length === 0) {
|
|
340
|
+
lines.push(" No AI platform incidents");
|
|
341
|
+
} else {
|
|
342
|
+
for (const platform of platforms) {
|
|
343
|
+
const label = platform.name || platform.platform || "Unknown";
|
|
344
|
+
const percentage = platform.percentage === undefined ? "" : ` (${platform.percentage}%)`;
|
|
345
|
+
lines.push(` ${label}: ${platform.count ?? 0}${percentage}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return `${lines.join("\n")}\n`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function appendKeyValueLines(lines, values, preferredOrder = null) {
|
|
353
|
+
const source = values && typeof values === "object" ? values : {};
|
|
354
|
+
const keys = preferredOrder || Object.keys(source);
|
|
355
|
+
if (keys.length === 0) {
|
|
356
|
+
lines.push(" None");
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
for (const key of keys) {
|
|
360
|
+
lines.push(` ${key}: ${source[key] ?? 0}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function formatIncidentsCsv(incidents) {
|
|
365
|
+
const columns = [
|
|
366
|
+
["incident_id", (incident) => incident.incident_id || incident.id || ""],
|
|
367
|
+
["created_at", (incident) => incident.created_at || ""],
|
|
368
|
+
["risk_level", (incident) => incident.risk_level || incident.severity || ""],
|
|
369
|
+
["type", (incident) => formatFindingTypes(incident)],
|
|
370
|
+
["user_email", (incident) => incident.user_email || ""],
|
|
371
|
+
["platform", (incident) => formatPlatform(incident)],
|
|
372
|
+
["decision", (incident) => formatDecision(incident)],
|
|
373
|
+
["source", (incident) => incident.source || ""],
|
|
374
|
+
["url", (incident) => incident.url || ""],
|
|
375
|
+
["blocked", (incident) => incident.blocked === undefined ? "" : String(Boolean(incident.blocked))],
|
|
376
|
+
["status", (incident) => incident.status || ""],
|
|
377
|
+
["file_name", (incident) => incident.file_name || ""]
|
|
378
|
+
];
|
|
379
|
+
|
|
380
|
+
const header = columns.map(([name]) => escapeCsv(name)).join(",");
|
|
381
|
+
const rows = incidents.map((incident) =>
|
|
382
|
+
columns.map(([, getter]) => escapeCsv(getter(incident))).join(",")
|
|
383
|
+
);
|
|
384
|
+
return `${[header, ...rows].join("\n")}\n`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function formatTable(rows, columns) {
|
|
388
|
+
const widths = columns.map((column) => {
|
|
389
|
+
const contentWidth = rows.reduce((max, row) => {
|
|
390
|
+
return Math.max(max, String(row[column.key] ?? "").length);
|
|
391
|
+
}, column.label.length);
|
|
392
|
+
return Math.min(contentWidth, column.maxWidth || contentWidth);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const renderRow = (row) => columns.map((column, index) => {
|
|
396
|
+
const value = truncate(String(row[column.key] ?? ""), widths[index]);
|
|
397
|
+
return value.padEnd(widths[index]);
|
|
398
|
+
}).join(" ");
|
|
399
|
+
|
|
400
|
+
const header = renderRow(Object.fromEntries(columns.map((column) => [column.key, column.label])));
|
|
401
|
+
const separator = widths.map((width) => "-".repeat(width)).join(" ");
|
|
402
|
+
const body = rows.map(renderRow).join("\n");
|
|
403
|
+
return `${header}\n${separator}\n${body}\n`;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function truncate(value, width) {
|
|
407
|
+
if (value.length <= width) {
|
|
408
|
+
return value;
|
|
409
|
+
}
|
|
410
|
+
if (width <= 3) {
|
|
411
|
+
return value.slice(0, width);
|
|
412
|
+
}
|
|
413
|
+
return `${value.slice(0, width - 3)}...`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function formatDate(value) {
|
|
417
|
+
if (!value) {
|
|
418
|
+
return "-";
|
|
419
|
+
}
|
|
420
|
+
const date = new Date(value);
|
|
421
|
+
if (Number.isNaN(date.getTime())) {
|
|
422
|
+
return String(value);
|
|
423
|
+
}
|
|
424
|
+
return date.toISOString();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function escapeCsv(value) {
|
|
428
|
+
if (value === null || value === undefined) {
|
|
429
|
+
return "";
|
|
430
|
+
}
|
|
431
|
+
const text = String(value);
|
|
432
|
+
if (/[",\n\r]/.test(text)) {
|
|
433
|
+
return `"${text.replaceAll("\"", "\"\"")}"`;
|
|
434
|
+
}
|
|
435
|
+
return text;
|
|
436
|
+
}
|