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.
@@ -0,0 +1,146 @@
1
+ import { requestApiKeyFormData } from "./api-client.js";
2
+ import { dash, formatTable } from "./table.js";
3
+
4
+ export const FAIL_ON_VALUES = ["alert", "block", "never"];
5
+
6
+ const DECISION_RANK = {
7
+ allow: 0,
8
+ alert: 1,
9
+ block: 2
10
+ };
11
+
12
+ export async function analyzeDlpBatch({
13
+ text = "",
14
+ files = [],
15
+ metadata = {},
16
+ apiKey,
17
+ env = process.env
18
+ }) {
19
+ const form = new FormData();
20
+ if (text) {
21
+ form.append("text", text);
22
+ }
23
+ for (const file of files) {
24
+ const blob = new Blob([file.data], {
25
+ type: file.contentType || "application/octet-stream"
26
+ });
27
+ form.append("files", blob, file.filename || "file");
28
+ }
29
+
30
+ for (const [key, value] of Object.entries(normalizeMetadata(metadata))) {
31
+ form.append(key, value);
32
+ }
33
+
34
+ return requestApiKeyFormData("/api/v1/dlp/analyze", form, {
35
+ apiKey,
36
+ env
37
+ });
38
+ }
39
+
40
+ export function parseFailOn(value, optionName = "--fail-on") {
41
+ const normalized = String(value || "").trim().toLowerCase();
42
+ if (!FAIL_ON_VALUES.includes(normalized)) {
43
+ throw new Error(`Invalid ${optionName}. Allowed values: ${FAIL_ON_VALUES.join(", ")}.`);
44
+ }
45
+ return normalized;
46
+ }
47
+
48
+ export function shouldFailDecision(decision, failOn) {
49
+ if (failOn === "never") {
50
+ return false;
51
+ }
52
+ const rank = DECISION_RANK[String(decision || "").toLowerCase()] ?? 0;
53
+ return rank >= DECISION_RANK[failOn];
54
+ }
55
+
56
+ export function sanitizeDlpResponse(response) {
57
+ const sanitized = deepClone(response || {});
58
+ delete sanitized.anonymizationMap;
59
+ for (const artifact of sanitized.artifacts || []) {
60
+ delete artifact.anonymizationMap;
61
+ }
62
+ return sanitized;
63
+ }
64
+
65
+ export function summarizeDlpResponses(responses, { failOn, includeSensitive = false } = {}) {
66
+ const normalized = responses.map((response) =>
67
+ includeSensitive ? response : sanitizeDlpResponse(response)
68
+ );
69
+ const worst = normalized.reduce((current, response) => {
70
+ const decision = String(response?.decision || "allow").toLowerCase();
71
+ return (DECISION_RANK[decision] ?? 0) > (DECISION_RANK[current] ?? 0)
72
+ ? decision
73
+ : current;
74
+ }, "allow");
75
+ const failed = normalized.some((response) => shouldFailDecision(response?.decision, failOn));
76
+
77
+ return {
78
+ decision: worst,
79
+ failed,
80
+ failOn,
81
+ totalRequests: normalized.length,
82
+ incidents: normalized.map((response) => response?.incident_id).filter(Boolean),
83
+ responses: normalized
84
+ };
85
+ }
86
+
87
+ export function formatDlpSummary(summary, { scannedFiles = 0, scannedText = false } = {}) {
88
+ const lines = [];
89
+ lines.push(`Scan decision: ${summary.decision}`);
90
+ lines.push(`Fail threshold: ${summary.failOn}`);
91
+ if (scannedText) {
92
+ lines.push("Text input: scanned");
93
+ }
94
+ lines.push(`Files scanned: ${scannedFiles}`);
95
+ if (summary.incidents.length) {
96
+ lines.push(`Incidents: ${summary.incidents.join(", ")}`);
97
+ }
98
+
99
+ const rows = summary.responses.map((response, index) => ({
100
+ request: index + 1,
101
+ decision: response?.decision || "-",
102
+ risk: response?.riskLevel || "-",
103
+ message: response?.message || "-",
104
+ incident: response?.incident_id || "-"
105
+ }));
106
+ if (rows.length) {
107
+ lines.push("");
108
+ lines.push(formatTable(rows, [
109
+ { header: "REQUEST", value: (row) => row.request },
110
+ { header: "DECISION", value: (row) => dash(row.decision) },
111
+ { header: "RISK", value: (row) => dash(row.risk) },
112
+ { header: "MESSAGE", value: (row) => dash(row.message) },
113
+ { header: "INCIDENT", value: (row) => dash(row.incident) }
114
+ ]));
115
+ }
116
+
117
+ return `${lines.join("\n")}\n`;
118
+ }
119
+
120
+ function normalizeMetadata(metadata) {
121
+ return {
122
+ source: stringOr(metadata.source, "cli"),
123
+ platform: stringOr(metadata.platform, "cli"),
124
+ hostname: stringOr(metadata.hostname, "unknown"),
125
+ path: normalizePathSuffix(metadata.path || "/cli"),
126
+ method: stringOr(metadata.method, "CLI"),
127
+ ...(metadata.userEmail ? { user_email: String(metadata.userEmail) } : {})
128
+ };
129
+ }
130
+
131
+ function normalizePathSuffix(value) {
132
+ const text = String(value || "").trim();
133
+ if (!text) {
134
+ return "/cli";
135
+ }
136
+ return text.startsWith("/") ? text : `/${text}`;
137
+ }
138
+
139
+ function stringOr(value, fallback) {
140
+ const text = String(value || "").trim();
141
+ return text || fallback;
142
+ }
143
+
144
+ function deepClone(value) {
145
+ return JSON.parse(JSON.stringify(value));
146
+ }
@@ -0,0 +1,11 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ const require = createRequire(import.meta.url);
4
+ const packageJson = require("../../package.json");
5
+
6
+ export function getPackageInfo() {
7
+ return {
8
+ name: packageJson.name,
9
+ version: packageJson.version
10
+ };
11
+ }
@@ -0,0 +1,55 @@
1
+ import { createInterface } from "node:readline";
2
+ import { createInterface as createPromisesInterface } from "node:readline/promises";
3
+
4
+ export async function promptText({
5
+ message,
6
+ stdin = process.stdin,
7
+ stdout = process.stdout,
8
+ optionName
9
+ }) {
10
+ assertInteractive(stdin, stdout, optionName);
11
+
12
+ const rl = createPromisesInterface({ input: stdin, output: stdout });
13
+ try {
14
+ return (await rl.question(message)).trim();
15
+ } finally {
16
+ rl.close();
17
+ }
18
+ }
19
+
20
+ export async function promptPassword({
21
+ message = "Password: ",
22
+ stdin = process.stdin,
23
+ stdout = process.stdout,
24
+ optionName = "--password"
25
+ }) {
26
+ assertInteractive(stdin, stdout, optionName);
27
+
28
+ return new Promise((resolve) => {
29
+ const rl = createInterface({ input: stdin, output: stdout, terminal: true });
30
+ const originalWrite = rl._writeToOutput;
31
+
32
+ rl._writeToOutput = function writeMuted(stringToWrite) {
33
+ if (rl.stdoutMuted) {
34
+ if (stringToWrite.includes("\n") || stringToWrite.includes("\r")) {
35
+ rl.output.write(stringToWrite.replace(/[^\r\n]/g, ""));
36
+ }
37
+ return;
38
+ }
39
+ originalWrite.call(rl, stringToWrite);
40
+ };
41
+
42
+ rl.question(message, (answer) => {
43
+ rl.close();
44
+ stdout.write("\n");
45
+ resolve(answer);
46
+ });
47
+ rl.stdoutMuted = true;
48
+ });
49
+ }
50
+
51
+ function assertInteractive(stdin, stdout, optionName) {
52
+ if (!stdin.isTTY || !stdout.isTTY) {
53
+ throw new Error(`${optionName} is required when running non-interactively`);
54
+ }
55
+ }
@@ -0,0 +1,64 @@
1
+ import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { getConfigDir } from "./auth-store.js";
5
+
6
+ const CURSOR_FILE_NAME = "siem-cursors.json";
7
+
8
+ export function buildSiemCursorKey({ apiUrl, cursorName = "default", category = null, severity = null }) {
9
+ const raw = JSON.stringify({
10
+ apiUrl: String(apiUrl || "").replace(/\/+$/, ""),
11
+ cursorName: cursorName || "default",
12
+ category: category || null,
13
+ severity: severity || null
14
+ });
15
+ return Buffer.from(raw, "utf8").toString("base64url");
16
+ }
17
+
18
+ export async function readSiemCursor(key, env = process.env) {
19
+ const store = await readCursorStore(env);
20
+ const record = store.cursors?.[key];
21
+ return typeof record?.cursor === "string" && record.cursor ? record.cursor : null;
22
+ }
23
+
24
+ export async function writeSiemCursor(key, record, env = process.env) {
25
+ const store = await readCursorStore(env);
26
+ store.cursors[key] = {
27
+ ...record,
28
+ updatedAt: new Date().toISOString()
29
+ };
30
+ await writeCursorStore(store, env);
31
+ }
32
+
33
+ async function readCursorStore(env) {
34
+ try {
35
+ const raw = await readFile(getCursorFilePath(env), "utf8");
36
+ const parsed = JSON.parse(raw);
37
+ return {
38
+ version: 1,
39
+ cursors: parsed?.cursors && typeof parsed.cursors === "object" ? parsed.cursors : {}
40
+ };
41
+ } catch (error) {
42
+ if (error?.code === "ENOENT") {
43
+ return { version: 1, cursors: {} };
44
+ }
45
+ throw error;
46
+ }
47
+ }
48
+
49
+ async function writeCursorStore(store, env) {
50
+ const dir = getConfigDir(env);
51
+ const filePath = getCursorFilePath(env);
52
+ const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
53
+
54
+ await mkdir(dir, { recursive: true, mode: 0o700 });
55
+ await chmod(dir, 0o700).catch(() => {});
56
+ await writeFile(tempPath, `${JSON.stringify(store, null, 2)}\n`, { mode: 0o600 });
57
+ await chmod(tempPath, 0o600).catch(() => {});
58
+ await rename(tempPath, filePath);
59
+ await chmod(filePath, 0o600).catch(() => {});
60
+ }
61
+
62
+ function getCursorFilePath(env) {
63
+ return path.join(getConfigDir(env), CURSOR_FILE_NAME);
64
+ }
@@ -0,0 +1,30 @@
1
+ export function formatTable(rows, columns) {
2
+ if (!rows.length) {
3
+ return "";
4
+ }
5
+
6
+ const widths = columns.map((column) => {
7
+ const headerLength = column.header.length;
8
+ const cellLengths = rows.map((row) => String(column.value(row)).length);
9
+ return Math.max(headerLength, ...cellLengths);
10
+ });
11
+
12
+ const header = columns
13
+ .map((column, index) => column.header.padEnd(widths[index]))
14
+ .join(" ");
15
+ const separator = widths.map((width) => "-".repeat(width)).join(" ");
16
+ const body = rows.map((row) =>
17
+ columns
18
+ .map((column, index) => String(column.value(row)).padEnd(widths[index]))
19
+ .join(" ")
20
+ );
21
+
22
+ return [header, separator, ...body].join("\n");
23
+ }
24
+
25
+ export function dash(value) {
26
+ if (value === null || value === undefined || value === "") {
27
+ return "-";
28
+ }
29
+ return String(value);
30
+ }
@@ -0,0 +1,33 @@
1
+ const DURATION_UNITS = {
2
+ m: 60 * 1000,
3
+ h: 60 * 60 * 1000,
4
+ d: 24 * 60 * 60 * 1000
5
+ };
6
+
7
+ export function parseDuration(value) {
8
+ const raw = String(value || "").trim();
9
+ const match = /^([1-9]\d*)([mhd])$/.exec(raw);
10
+ if (!match) {
11
+ throw new Error("Duration must be a positive integer followed by m, h, or d, for example 30m, 12h, or 7d.");
12
+ }
13
+
14
+ const amount = Number(match[1]);
15
+ const unit = match[2];
16
+ return {
17
+ raw,
18
+ amount,
19
+ unit,
20
+ milliseconds: amount * DURATION_UNITS[unit]
21
+ };
22
+ }
23
+
24
+ export function parseApiTimestamp(value) {
25
+ if (!value) {
26
+ return null;
27
+ }
28
+
29
+ const raw = String(value);
30
+ const normalized = /[zZ]|[+-]\d{2}:?\d{2}$/.test(raw) ? raw : `${raw}Z`;
31
+ const parsed = new Date(normalized);
32
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
33
+ }
@@ -0,0 +1,24 @@
1
+ export function compareVersions(a, b) {
2
+ const parse = (value) =>
3
+ String(value || "")
4
+ .replace(/^v/, "")
5
+ .split("-")[0]
6
+ .split(".")
7
+ .map(Number);
8
+
9
+ const left = parse(a);
10
+ const right = parse(b);
11
+
12
+ if (left.some(Number.isNaN) || right.some(Number.isNaN)) {
13
+ return 0;
14
+ }
15
+
16
+ for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
17
+ const diff = (left[i] || 0) - (right[i] || 0);
18
+ if (diff !== 0) {
19
+ return diff;
20
+ }
21
+ }
22
+
23
+ return 0;
24
+ }