|
|
|
@@ -1,6 +1,6 @@
|
|
|
|
|
(function () {
|
|
|
|
|
window.__awRuPatchVersion = "template-v2";
|
|
|
|
|
document.documentElement.setAttribute("data-aw-ru-patch", "template-v2");
|
|
|
|
|
window.__awRuPatchVersion = "template-v4-dlp-review";
|
|
|
|
|
document.documentElement.setAttribute("data-aw-ru-patch", "template-v4-dlp-review");
|
|
|
|
|
|
|
|
|
|
const exact = new Map([
|
|
|
|
|
["ActivityWatch", "АктивВотч"],
|
|
|
|
@@ -259,6 +259,13 @@
|
|
|
|
|
/\/raw\b/i
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const dlpVerdictOptions = [
|
|
|
|
|
{ value: "false_positive", label: "Ложное срабатывание" },
|
|
|
|
|
{ value: "allowed", label: "Разрешено" },
|
|
|
|
|
{ value: "review_needed", label: "На проверку" },
|
|
|
|
|
{ value: "incident", label: "Инцидент" }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
function replaceText(text) {
|
|
|
|
|
if (!text) return text;
|
|
|
|
|
if (exact.has(text.trim())) {
|
|
|
|
@@ -301,7 +308,21 @@
|
|
|
|
|
style.id = "aw-ru-hide-noise-style";
|
|
|
|
|
style.textContent = [
|
|
|
|
|
'[href*="/raw-data"], [href*="/raw"], a[data-testid*="raw"], button[data-testid*="raw"] { display: none !important; }',
|
|
|
|
|
'[aria-label="Raw Data"], [aria-label="Сырые данные"] { display: none !important; }'
|
|
|
|
|
'[aria-label="Raw Data"], [aria-label="Сырые данные"] { display: none !important; }',
|
|
|
|
|
'.aw-ru-dlp-center { margin: 16px 0; padding: 16px; border: 1px solid rgba(120,120,120,.35); border-radius: 8px; background: rgba(20,20,20,.03); }',
|
|
|
|
|
'.aw-ru-dlp-toolbar { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; margin-bottom: 12px; }',
|
|
|
|
|
'.aw-ru-dlp-toolbar input, .aw-ru-dlp-toolbar select, .aw-ru-dlp-toolbar textarea { min-height: 32px; }',
|
|
|
|
|
'.aw-ru-dlp-toolbar button, .aw-ru-dlp-row button { min-height: 32px; padding: 4px 10px; }',
|
|
|
|
|
'.aw-ru-dlp-table { width: 100%; border-collapse: collapse; font-size: 13px; }',
|
|
|
|
|
'.aw-ru-dlp-table th, .aw-ru-dlp-table td { border: 1px solid rgba(120,120,120,.25); padding: 6px; vertical-align: top; }',
|
|
|
|
|
'.aw-ru-dlp-table td input, .aw-ru-dlp-table td select { width: 100%; box-sizing: border-box; }',
|
|
|
|
|
'.aw-ru-dlp-muted { opacity: .55; }',
|
|
|
|
|
'.aw-ru-dlp-pill { display: inline-block; padding: 2px 8px; border-radius: 999px; background: rgba(90,140,255,.15); font-size: 12px; }',
|
|
|
|
|
'.aw-ru-dlp-status { margin-left: auto; font-size: 12px; opacity: .8; }',
|
|
|
|
|
'.aw-ru-dlp-message { margin-top: 8px; font-size: 12px; }',
|
|
|
|
|
'.aw-ru-dlp-actions { display: flex; gap: 6px; flex-wrap: wrap; }',
|
|
|
|
|
'.aw-ru-dlp-section { margin-top: 18px; }',
|
|
|
|
|
'.aw-ru-dlp-section h5 { margin: 0 0 8px; }'
|
|
|
|
|
].join("\n");
|
|
|
|
|
document.head.appendChild(style);
|
|
|
|
|
}
|
|
|
|
@@ -318,7 +339,639 @@
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getCurrentHostFromHash() {
|
|
|
|
|
const hash = window.location.hash || "";
|
|
|
|
|
const activityMatch = hash.match(/#\/activity\/([^/?#]+)/);
|
|
|
|
|
if (activityMatch && activityMatch[1]) return decodeURIComponent(activityMatch[1]);
|
|
|
|
|
const trendsMatch = hash.match(/#\/trends\/([^/?#]+)/);
|
|
|
|
|
if (trendsMatch && trendsMatch[1]) return decodeURIComponent(trendsMatch[1]);
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getDlpHostFromSettings(settings) {
|
|
|
|
|
const routeHost = getCurrentHostFromHash();
|
|
|
|
|
if (routeHost) return routeHost;
|
|
|
|
|
const bucketHost = getDlpHostFromBucketId(getDlpBucketIdFromHash());
|
|
|
|
|
if (bucketHost) return bucketHost;
|
|
|
|
|
return getTrendsHostFromSettings(settings);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getDlpHref(host) {
|
|
|
|
|
if (!host) return "#/buckets";
|
|
|
|
|
return "#/buckets/" + encodeURIComponent("aw-dlp-endpoint-signals_" + host);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isDlpSignalBucketRoute() {
|
|
|
|
|
return /^#\/buckets\/aw-dlp-endpoint-signals_/i.test(window.location.hash || "");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getDlpBucketIdFromHash() {
|
|
|
|
|
const hash = window.location.hash || "";
|
|
|
|
|
const match = hash.match(/^#\/buckets\/([^/?#]+)/i);
|
|
|
|
|
return match && match[1] ? decodeURIComponent(match[1]) : "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getDlpHostFromBucketId(bucketId) {
|
|
|
|
|
const prefix = "aw-dlp-endpoint-signals_";
|
|
|
|
|
return bucketId.startsWith(prefix) ? bucketId.slice(prefix.length) : "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function escapeHtml(value) {
|
|
|
|
|
return String(value == null ? "" : value)
|
|
|
|
|
.replace(/&/g, "&")
|
|
|
|
|
.replace(/</g, "<")
|
|
|
|
|
.replace(/>/g, ">")
|
|
|
|
|
.replace(/"/g, """)
|
|
|
|
|
.replace(/'/g, "'");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeText(value) {
|
|
|
|
|
return String(value == null ? "" : value).trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildDlpKey(event) {
|
|
|
|
|
const data = event && event.data ? event.data : {};
|
|
|
|
|
return [
|
|
|
|
|
event && event.timestamp || "",
|
|
|
|
|
data.signalType || "",
|
|
|
|
|
data.username || "",
|
|
|
|
|
data.owner || "",
|
|
|
|
|
data.documentName || "",
|
|
|
|
|
data.printerName || ""
|
|
|
|
|
].join("|");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function generateDlpId(prefix) {
|
|
|
|
|
return prefix + "-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function awApiJson(url, options) {
|
|
|
|
|
const response = await fetch(url, Object.assign({
|
|
|
|
|
credentials: "same-origin",
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/json; charset=utf-8"
|
|
|
|
|
}
|
|
|
|
|
}, options || {}));
|
|
|
|
|
if (response.status === 304 || response.status === 409) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error("aw-api-" + response.status);
|
|
|
|
|
}
|
|
|
|
|
if (response.status === 204) return null;
|
|
|
|
|
const text = await response.text();
|
|
|
|
|
return text ? JSON.parse(text) : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function ensureAwBucket(bucketId, clientName, bucketType, hostname) {
|
|
|
|
|
await awApiJson("/api/0/buckets/" + encodeURIComponent(bucketId), {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
client: clientName,
|
|
|
|
|
type: bucketType,
|
|
|
|
|
hostname: hostname
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveAwHeartbeat(bucketId, payload, pulsetimeSeconds) {
|
|
|
|
|
const pulsetime = pulsetimeSeconds || 1;
|
|
|
|
|
await awApiJson("/api/0/buckets/" + encodeURIComponent(bucketId) + "/heartbeat?pulsetime=" + pulsetime, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify(payload)
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadBucketEvents(bucketId, limit) {
|
|
|
|
|
const data = await awApiJson("/api/0/buckets/" + encodeURIComponent(bucketId) + "/events?limit=" + (limit || 100), {
|
|
|
|
|
method: "GET",
|
|
|
|
|
headers: {}
|
|
|
|
|
});
|
|
|
|
|
return Array.isArray(data) ? data : [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getRuleMatchFields(event) {
|
|
|
|
|
const data = event && event.data ? event.data : {};
|
|
|
|
|
return {
|
|
|
|
|
signalType: normalizeText(data.signalType),
|
|
|
|
|
username: normalizeText(data.username),
|
|
|
|
|
owner: normalizeText(data.owner),
|
|
|
|
|
documentName: normalizeText(data.documentName),
|
|
|
|
|
printerName: normalizeText(data.printerName),
|
|
|
|
|
hostname: normalizeText(data.hostname)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function serializeRuleMatch(match) {
|
|
|
|
|
const normalized = match || {};
|
|
|
|
|
return [
|
|
|
|
|
normalized.signalType || "",
|
|
|
|
|
normalized.username || "",
|
|
|
|
|
normalized.owner || "",
|
|
|
|
|
normalized.documentName || "",
|
|
|
|
|
normalized.printerName || "",
|
|
|
|
|
normalized.hostname || ""
|
|
|
|
|
].join("|");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getRuleId(ruleEvent) {
|
|
|
|
|
const data = ruleEvent && ruleEvent.data ? ruleEvent.data : {};
|
|
|
|
|
return normalizeText(data.ruleId) || [
|
|
|
|
|
serializeRuleMatch(data.match || {}),
|
|
|
|
|
normalizeText(data.category),
|
|
|
|
|
normalizeText(data.action)
|
|
|
|
|
].join("|");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getReviewId(reviewEvent) {
|
|
|
|
|
const data = reviewEvent && reviewEvent.data ? reviewEvent.data : {};
|
|
|
|
|
const review = data.review || {};
|
|
|
|
|
return normalizeText(review.reviewId) || [
|
|
|
|
|
data.sourceEvent && data.sourceEvent.timestamp || "",
|
|
|
|
|
data.sourceEvent && data.sourceEvent.data ? buildDlpKey({ timestamp: data.sourceEvent.timestamp, data: data.sourceEvent.data }) : "",
|
|
|
|
|
normalizeText(review.verdict),
|
|
|
|
|
normalizeText(review.category)
|
|
|
|
|
].join("|");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function collapseRuleEvents(events) {
|
|
|
|
|
const ordered = (events || []).slice().sort(function (a, b) {
|
|
|
|
|
return String(a.timestamp).localeCompare(String(b.timestamp));
|
|
|
|
|
});
|
|
|
|
|
const map = new Map();
|
|
|
|
|
ordered.forEach(function (event) {
|
|
|
|
|
map.set(getRuleId(event), event);
|
|
|
|
|
});
|
|
|
|
|
return Array.from(map.values()).sort(function (a, b) {
|
|
|
|
|
return String(b.timestamp).localeCompare(String(a.timestamp));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function collapseReviewEvents(events) {
|
|
|
|
|
const ordered = (events || []).slice().sort(function (a, b) {
|
|
|
|
|
return String(a.timestamp).localeCompare(String(b.timestamp));
|
|
|
|
|
});
|
|
|
|
|
const map = new Map();
|
|
|
|
|
ordered.forEach(function (event) {
|
|
|
|
|
map.set(getReviewId(event), event);
|
|
|
|
|
});
|
|
|
|
|
return Array.from(map.values()).sort(function (a, b) {
|
|
|
|
|
return String(b.timestamp).localeCompare(String(a.timestamp));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ruleMatchesEvent(rule, event) {
|
|
|
|
|
const eventFields = getRuleMatchFields(event);
|
|
|
|
|
const match = rule && rule.data && rule.data.match ? rule.data.match : {};
|
|
|
|
|
return Object.keys(eventFields).every(function (key) {
|
|
|
|
|
const ruleValue = normalizeText(match[key]);
|
|
|
|
|
return !ruleValue || ruleValue === eventFields[key];
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getSuppressionState() {
|
|
|
|
|
if (!window.__awRuDlpState) {
|
|
|
|
|
window.__awRuDlpState = {
|
|
|
|
|
rules: [],
|
|
|
|
|
activeRules: [],
|
|
|
|
|
reviews: [],
|
|
|
|
|
events: [],
|
|
|
|
|
loading: false
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return window.__awRuDlpState;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeBadDlpLinks(root) {
|
|
|
|
|
const badLinks = root.querySelectorAll("a[href*='/view/DLP']");
|
|
|
|
|
badLinks.forEach(function (link) {
|
|
|
|
|
const item = link.closest("li") || link;
|
|
|
|
|
item.remove();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateDlpLinks(root, href) {
|
|
|
|
|
const links = root.querySelectorAll("a[data-aw-ru-dlp-link='1']");
|
|
|
|
|
for (const link of links) {
|
|
|
|
|
if (link.getAttribute("href") !== href) {
|
|
|
|
|
link.setAttribute("href", href);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildDlpNavItem(templateItem, href) {
|
|
|
|
|
const templateLink = templateItem.querySelector("a[href], [role='link']");
|
|
|
|
|
if (!templateLink) return null;
|
|
|
|
|
|
|
|
|
|
const item = document.createElement("li");
|
|
|
|
|
item.setAttribute("data-aw-ru-dlp-item", "1");
|
|
|
|
|
item.className = templateItem.className || "";
|
|
|
|
|
|
|
|
|
|
const link = document.createElement("a");
|
|
|
|
|
link.setAttribute("href", href);
|
|
|
|
|
link.setAttribute("data-aw-ru-dlp-link", "1");
|
|
|
|
|
link.textContent = "DLP";
|
|
|
|
|
link.className = templateLink.className || "";
|
|
|
|
|
|
|
|
|
|
item.appendChild(link);
|
|
|
|
|
return item;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function findPrimaryNavList(root) {
|
|
|
|
|
const navLists = Array.from(root.querySelectorAll("nav ul"));
|
|
|
|
|
return navLists.find(function (list) {
|
|
|
|
|
const labels = Array.from(list.querySelectorAll("a"))
|
|
|
|
|
.map(function (link) { return normalizeText(link.textContent); })
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
return labels.includes("Главная") ||
|
|
|
|
|
labels.includes("Home") ||
|
|
|
|
|
labels.includes("Активность") ||
|
|
|
|
|
labels.includes("Activity");
|
|
|
|
|
}) || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function injectDlpNavigation(root) {
|
|
|
|
|
const href = getDlpHref(window.__awRuPatchSettingsHost || getCurrentHostFromHash());
|
|
|
|
|
removeBadDlpLinks(root);
|
|
|
|
|
updateDlpLinks(root, href);
|
|
|
|
|
if (root.querySelector("[data-aw-ru-dlp-item='1']")) return;
|
|
|
|
|
|
|
|
|
|
const primaryNav = findPrimaryNavList(root);
|
|
|
|
|
if (primaryNav) {
|
|
|
|
|
const templateItem = primaryNav.querySelector("li") || primaryNav.parentElement;
|
|
|
|
|
const dlpItem = templateItem ? buildDlpNavItem(templateItem, href) : null;
|
|
|
|
|
if (dlpItem) {
|
|
|
|
|
primaryNav.appendChild(dlpItem);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const item = document.createElement("li");
|
|
|
|
|
item.setAttribute("data-aw-ru-dlp-item", "1");
|
|
|
|
|
const link = document.createElement("a");
|
|
|
|
|
link.setAttribute("href", href);
|
|
|
|
|
link.setAttribute("data-aw-ru-dlp-link", "1");
|
|
|
|
|
link.textContent = "DLP";
|
|
|
|
|
item.appendChild(link);
|
|
|
|
|
primaryNav.appendChild(item);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderDlpTableRows(center, host) {
|
|
|
|
|
const state = getSuppressionState();
|
|
|
|
|
const tbody = center.querySelector("[data-aw-ru-dlp-events]");
|
|
|
|
|
if (!tbody) return;
|
|
|
|
|
const hideSuppressed = center.querySelector("[data-aw-ru-hide-suppressed]") && center.querySelector("[data-aw-ru-hide-suppressed]").checked;
|
|
|
|
|
const rows = [];
|
|
|
|
|
for (const event of state.events) {
|
|
|
|
|
const matchedRule = state.activeRules.find(function (rule) { return ruleMatchesEvent(rule, event); }) || null;
|
|
|
|
|
if (hideSuppressed && matchedRule) continue;
|
|
|
|
|
const data = event.data || {};
|
|
|
|
|
const eventKey = buildDlpKey(event);
|
|
|
|
|
rows.push(
|
|
|
|
|
'<tr class="aw-ru-dlp-row' + (matchedRule ? ' aw-ru-dlp-muted' : '') + '" data-aw-ru-dlp-key="' + escapeHtml(eventKey) + '">' +
|
|
|
|
|
"<td>" + escapeHtml(new Date(event.timestamp).toLocaleString()) + "</td>" +
|
|
|
|
|
"<td>" + escapeHtml(data.signalType || "") + "</td>" +
|
|
|
|
|
"<td>" + escapeHtml(data.username || data.owner || "") + "</td>" +
|
|
|
|
|
"<td>" + escapeHtml(data.documentName || "") + "</td>" +
|
|
|
|
|
"<td>" + escapeHtml(data.printerName || "") + "</td>" +
|
|
|
|
|
"<td>" + (matchedRule ? '<span class="aw-ru-dlp-pill">Подавлено правилом</span>' : "") + "</td>" +
|
|
|
|
|
'<td><select data-aw-ru-dlp-verdict>' +
|
|
|
|
|
dlpVerdictOptions.map(function (option) {
|
|
|
|
|
return '<option value="' + option.value + '">' + option.label + '</option>';
|
|
|
|
|
}).join("") +
|
|
|
|
|
"</select></td>" +
|
|
|
|
|
'<td><input type="text" data-aw-ru-dlp-category placeholder="например, safe.print.invoice" /></td>' +
|
|
|
|
|
'<td><input type="text" data-aw-ru-dlp-comment placeholder="комментарий" /></td>' +
|
|
|
|
|
'<td class="aw-ru-dlp-actions">' +
|
|
|
|
|
'<button type="button" data-aw-ru-save-review>Сохранить</button>' +
|
|
|
|
|
'<button type="button" data-aw-ru-save-rule>Правило</button>' +
|
|
|
|
|
"</td>" +
|
|
|
|
|
"</tr>"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
tbody.innerHTML = rows.length ? rows.join("") : '<tr><td colspan="10">Нет DLP-событий в выборке.</td></tr>';
|
|
|
|
|
center.querySelector("[data-aw-ru-dlp-status]").textContent =
|
|
|
|
|
"Событий: " + state.events.length + " · правил: " + state.activeRules.length + "/" + state.rules.length + " · review: " + state.reviews.filter(function (review) { return !(review.data && review.data.review && review.data.review.archived); }).length + "/" + state.reviews.length;
|
|
|
|
|
bindDlpRowActions(center, host);
|
|
|
|
|
renderDlpRuleManager(center, host);
|
|
|
|
|
renderDlpReviewManager(center, host);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveDlpReview(host, event, row) {
|
|
|
|
|
const bucketId = "aw-dlp-review_" + host;
|
|
|
|
|
await ensureAwBucket(bucketId, "aw-dlp-review", "aw.dlp.review", host);
|
|
|
|
|
const verdict = row.querySelector("[data-aw-ru-dlp-verdict]").value;
|
|
|
|
|
const category = row.querySelector("[data-aw-ru-dlp-category]").value.trim();
|
|
|
|
|
const comment = row.querySelector("[data-aw-ru-dlp-comment]").value.trim();
|
|
|
|
|
await saveAwHeartbeat(bucketId, {
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
duration: 0,
|
|
|
|
|
data: {
|
|
|
|
|
host: host,
|
|
|
|
|
sourceBucket: getDlpBucketIdFromHash(),
|
|
|
|
|
sourceEvent: {
|
|
|
|
|
timestamp: event.timestamp,
|
|
|
|
|
data: event.data || {}
|
|
|
|
|
},
|
|
|
|
|
review: {
|
|
|
|
|
reviewId: generateDlpId("review"),
|
|
|
|
|
verdict: verdict,
|
|
|
|
|
category: category,
|
|
|
|
|
comment: comment,
|
|
|
|
|
archived: false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveDlpRule(host, event, row) {
|
|
|
|
|
const bucketId = "aw-dlp-rules_" + host;
|
|
|
|
|
await ensureAwBucket(bucketId, "aw-dlp-rules", "aw.dlp.rule", host);
|
|
|
|
|
const verdict = row.querySelector("[data-aw-ru-dlp-verdict]").value;
|
|
|
|
|
const category = row.querySelector("[data-aw-ru-dlp-category]").value.trim();
|
|
|
|
|
const comment = row.querySelector("[data-aw-ru-dlp-comment]").value.trim();
|
|
|
|
|
await saveAwHeartbeat(bucketId, {
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
duration: 0,
|
|
|
|
|
data: {
|
|
|
|
|
host: host,
|
|
|
|
|
ruleId: generateDlpId("rule"),
|
|
|
|
|
enabled: true,
|
|
|
|
|
action: verdict,
|
|
|
|
|
category: category,
|
|
|
|
|
comment: comment,
|
|
|
|
|
match: getRuleMatchFields(event)
|
|
|
|
|
}
|
|
|
|
|
}, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function bindDlpRowActions(center, host) {
|
|
|
|
|
const state = getSuppressionState();
|
|
|
|
|
const rows = center.querySelectorAll("[data-aw-ru-dlp-key]");
|
|
|
|
|
rows.forEach(function (row) {
|
|
|
|
|
if (row.getAttribute("data-aw-ru-bound") === "1") return;
|
|
|
|
|
row.setAttribute("data-aw-ru-bound", "1");
|
|
|
|
|
const eventKey = row.getAttribute("data-aw-ru-dlp-key");
|
|
|
|
|
const event = state.events.find(function (item) { return buildDlpKey(item) === eventKey; });
|
|
|
|
|
if (!event) return;
|
|
|
|
|
row.querySelector("[data-aw-ru-save-review]").addEventListener("click", async function () {
|
|
|
|
|
const message = center.querySelector("[data-aw-ru-dlp-message]");
|
|
|
|
|
try {
|
|
|
|
|
await saveDlpReview(host, event, row);
|
|
|
|
|
state.reviews = collapseReviewEvents(await loadBucketEvents("aw-dlp-review_" + host, 200));
|
|
|
|
|
renderDlpTableRows(center, host);
|
|
|
|
|
message.textContent = "Review сохранен.";
|
|
|
|
|
} catch (error) {
|
|
|
|
|
message.textContent = "Ошибка сохранения review: " + error.message;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
row.querySelector("[data-aw-ru-save-rule]").addEventListener("click", async function () {
|
|
|
|
|
const message = center.querySelector("[data-aw-ru-dlp-message]");
|
|
|
|
|
try {
|
|
|
|
|
await saveDlpRule(host, event, row);
|
|
|
|
|
state.rules = collapseRuleEvents(await loadBucketEvents("aw-dlp-rules_" + host, 200));
|
|
|
|
|
state.activeRules = state.rules.filter(function (rule) { return !(rule.data && rule.data.enabled === false); });
|
|
|
|
|
message.textContent = "Правило сохранено.";
|
|
|
|
|
renderDlpTableRows(center, host);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
message.textContent = "Ошибка сохранения правила: " + error.message;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function setDlpRuleEnabled(host, ruleEvent, enabled) {
|
|
|
|
|
const bucketId = "aw-dlp-rules_" + host;
|
|
|
|
|
await ensureAwBucket(bucketId, "aw-dlp-rules", "aw.dlp.rule", host);
|
|
|
|
|
const data = ruleEvent.data || {};
|
|
|
|
|
await saveAwHeartbeat(bucketId, {
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
duration: 0,
|
|
|
|
|
data: {
|
|
|
|
|
host: host,
|
|
|
|
|
ruleId: getRuleId(ruleEvent),
|
|
|
|
|
enabled: enabled,
|
|
|
|
|
action: data.action || "",
|
|
|
|
|
category: data.category || "",
|
|
|
|
|
comment: data.comment || "",
|
|
|
|
|
match: data.match || {}
|
|
|
|
|
}
|
|
|
|
|
}, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function setDlpReviewArchived(host, reviewEvent, archived) {
|
|
|
|
|
const bucketId = "aw-dlp-review_" + host;
|
|
|
|
|
await ensureAwBucket(bucketId, "aw-dlp-review", "aw.dlp.review", host);
|
|
|
|
|
const data = reviewEvent.data || {};
|
|
|
|
|
const review = data.review || {};
|
|
|
|
|
await saveAwHeartbeat(bucketId, {
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
duration: 0,
|
|
|
|
|
data: {
|
|
|
|
|
host: host,
|
|
|
|
|
sourceBucket: data.sourceBucket || getDlpBucketIdFromHash(),
|
|
|
|
|
sourceEvent: data.sourceEvent || {},
|
|
|
|
|
review: {
|
|
|
|
|
reviewId: getReviewId(reviewEvent),
|
|
|
|
|
verdict: review.verdict || "",
|
|
|
|
|
category: review.category || "",
|
|
|
|
|
comment: review.comment || "",
|
|
|
|
|
archived: archived
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderDlpRuleManager(center, host) {
|
|
|
|
|
const state = getSuppressionState();
|
|
|
|
|
const tbody = center.querySelector("[data-aw-ru-dlp-rules]");
|
|
|
|
|
if (!tbody) return;
|
|
|
|
|
const showDisabled = center.querySelector("[data-aw-ru-show-disabled-rules]") && center.querySelector("[data-aw-ru-show-disabled-rules]").checked;
|
|
|
|
|
const rows = [];
|
|
|
|
|
state.rules.forEach(function (ruleEvent) {
|
|
|
|
|
const data = ruleEvent.data || {};
|
|
|
|
|
const isEnabled = data.enabled !== false;
|
|
|
|
|
if (!isEnabled && !showDisabled) return;
|
|
|
|
|
rows.push(
|
|
|
|
|
'<tr data-aw-ru-rule-id="' + escapeHtml(getRuleId(ruleEvent)) + '">' +
|
|
|
|
|
'<td>' + escapeHtml(new Date(ruleEvent.timestamp).toLocaleString()) + '</td>' +
|
|
|
|
|
'<td>' + (isEnabled ? '<span class="aw-ru-dlp-pill">Активно</span>' : '<span class="aw-ru-dlp-pill">Отключено</span>') + '</td>' +
|
|
|
|
|
'<td>' + escapeHtml(data.action || "") + '</td>' +
|
|
|
|
|
'<td>' + escapeHtml(data.category || "") + '</td>' +
|
|
|
|
|
'<td>' + escapeHtml(serializeRuleMatch(data.match || {})) + '</td>' +
|
|
|
|
|
'<td>' + escapeHtml(data.comment || "") + '</td>' +
|
|
|
|
|
'<td class="aw-ru-dlp-actions">' +
|
|
|
|
|
'<button type="button" data-aw-ru-toggle-rule>' + (isEnabled ? 'Отключить' : 'Включить') + '</button>' +
|
|
|
|
|
'</td>' +
|
|
|
|
|
'</tr>'
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
tbody.innerHTML = rows.length ? rows.join("") : '<tr><td colspan="7">Сохраненных правил нет.</td></tr>';
|
|
|
|
|
tbody.querySelectorAll("[data-aw-ru-toggle-rule]").forEach(function (button) {
|
|
|
|
|
button.addEventListener("click", async function () {
|
|
|
|
|
const row = button.closest("[data-aw-ru-rule-id]");
|
|
|
|
|
const ruleId = row && row.getAttribute("data-aw-ru-rule-id");
|
|
|
|
|
const ruleEvent = state.rules.find(function (item) { return getRuleId(item) === ruleId; });
|
|
|
|
|
if (!ruleEvent) return;
|
|
|
|
|
const message = center.querySelector("[data-aw-ru-dlp-message]");
|
|
|
|
|
try {
|
|
|
|
|
await setDlpRuleEnabled(host, ruleEvent, ruleEvent.data && ruleEvent.data.enabled === false);
|
|
|
|
|
state.rules = collapseRuleEvents(await loadBucketEvents("aw-dlp-rules_" + host, 200));
|
|
|
|
|
state.activeRules = state.rules.filter(function (rule) { return !(rule.data && rule.data.enabled === false); });
|
|
|
|
|
renderDlpTableRows(center, host);
|
|
|
|
|
message.textContent = "Статус правила обновлен.";
|
|
|
|
|
} catch (error) {
|
|
|
|
|
message.textContent = "Ошибка обновления правила: " + error.message;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderDlpReviewManager(center, host) {
|
|
|
|
|
const state = getSuppressionState();
|
|
|
|
|
const tbody = center.querySelector("[data-aw-ru-dlp-reviews]");
|
|
|
|
|
if (!tbody) return;
|
|
|
|
|
const showArchived = center.querySelector("[data-aw-ru-show-archived-reviews]") && center.querySelector("[data-aw-ru-show-archived-reviews]").checked;
|
|
|
|
|
const rows = [];
|
|
|
|
|
state.reviews.forEach(function (reviewEvent) {
|
|
|
|
|
const data = reviewEvent.data || {};
|
|
|
|
|
const review = data.review || {};
|
|
|
|
|
const archived = review.archived === true;
|
|
|
|
|
if (archived && !showArchived) return;
|
|
|
|
|
const sourceData = data.sourceEvent && data.sourceEvent.data ? data.sourceEvent.data : {};
|
|
|
|
|
rows.push(
|
|
|
|
|
'<tr data-aw-ru-review-id="' + escapeHtml(getReviewId(reviewEvent)) + '">' +
|
|
|
|
|
'<td>' + escapeHtml(new Date(reviewEvent.timestamp).toLocaleString()) + '</td>' +
|
|
|
|
|
'<td>' + (archived ? '<span class="aw-ru-dlp-pill">Архив</span>' : '<span class="aw-ru-dlp-pill">Активно</span>') + '</td>' +
|
|
|
|
|
'<td>' + escapeHtml(review.verdict || "") + '</td>' +
|
|
|
|
|
'<td>' + escapeHtml(review.category || "") + '</td>' +
|
|
|
|
|
'<td>' + escapeHtml(review.comment || "") + '</td>' +
|
|
|
|
|
'<td>' + escapeHtml(sourceData.signalType || "") + ' · ' + escapeHtml(sourceData.documentName || sourceData.printerName || sourceData.username || "") + '</td>' +
|
|
|
|
|
'<td class="aw-ru-dlp-actions">' +
|
|
|
|
|
'<button type="button" data-aw-ru-toggle-review>' + (archived ? 'Вернуть' : 'Архивировать') + '</button>' +
|
|
|
|
|
'</td>' +
|
|
|
|
|
'</tr>'
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
tbody.innerHTML = rows.length ? rows.join("") : '<tr><td colspan="7">Сохраненных review нет.</td></tr>';
|
|
|
|
|
tbody.querySelectorAll("[data-aw-ru-toggle-review]").forEach(function (button) {
|
|
|
|
|
button.addEventListener("click", async function () {
|
|
|
|
|
const row = button.closest("[data-aw-ru-review-id]");
|
|
|
|
|
const reviewId = row && row.getAttribute("data-aw-ru-review-id");
|
|
|
|
|
const reviewEvent = state.reviews.find(function (item) { return getReviewId(item) === reviewId; });
|
|
|
|
|
if (!reviewEvent) return;
|
|
|
|
|
const message = center.querySelector("[data-aw-ru-dlp-message]");
|
|
|
|
|
try {
|
|
|
|
|
await setDlpReviewArchived(host, reviewEvent, !(reviewEvent.data && reviewEvent.data.review && reviewEvent.data.review.archived === true));
|
|
|
|
|
state.reviews = collapseReviewEvents(await loadBucketEvents("aw-dlp-review_" + host, 200));
|
|
|
|
|
renderDlpTableRows(center, host);
|
|
|
|
|
message.textContent = "Статус review обновлен.";
|
|
|
|
|
} catch (error) {
|
|
|
|
|
message.textContent = "Ошибка обновления review: " + error.message;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function refreshDlpCenter(center, host) {
|
|
|
|
|
const state = getSuppressionState();
|
|
|
|
|
if (state.loading) return;
|
|
|
|
|
state.loading = true;
|
|
|
|
|
center.querySelector("[data-aw-ru-dlp-message]").textContent = "Загрузка DLP-событий...";
|
|
|
|
|
try {
|
|
|
|
|
state.events = (await loadBucketEvents(getDlpBucketIdFromHash(), 200))
|
|
|
|
|
.sort(function (a, b) { return String(b.timestamp).localeCompare(String(a.timestamp)); });
|
|
|
|
|
try {
|
|
|
|
|
state.rules = collapseRuleEvents(await loadBucketEvents("aw-dlp-rules_" + host, 200));
|
|
|
|
|
state.activeRules = state.rules.filter(function (rule) { return !(rule.data && rule.data.enabled === false); });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
state.rules = [];
|
|
|
|
|
state.activeRules = [];
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
state.reviews = collapseReviewEvents(await loadBucketEvents("aw-dlp-review_" + host, 200));
|
|
|
|
|
} catch (error) {
|
|
|
|
|
state.reviews = [];
|
|
|
|
|
}
|
|
|
|
|
renderDlpTableRows(center, host);
|
|
|
|
|
center.querySelector("[data-aw-ru-dlp-message]").textContent = "DLP review центр обновлен.";
|
|
|
|
|
} catch (error) {
|
|
|
|
|
center.querySelector("[data-aw-ru-dlp-message]").textContent = "Ошибка загрузки DLP-событий: " + error.message;
|
|
|
|
|
} finally {
|
|
|
|
|
state.loading = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function injectDlpReviewCenter(root) {
|
|
|
|
|
if (!isDlpSignalBucketRoute()) return;
|
|
|
|
|
const bucketId = getDlpBucketIdFromHash();
|
|
|
|
|
const host = getDlpHostFromBucketId(bucketId);
|
|
|
|
|
if (!host) return;
|
|
|
|
|
|
|
|
|
|
const heading = root.querySelector("h3");
|
|
|
|
|
if (!heading) return;
|
|
|
|
|
|
|
|
|
|
let center = root.querySelector("[data-aw-ru-dlp-center='1']");
|
|
|
|
|
if (!center) {
|
|
|
|
|
center = document.createElement("section");
|
|
|
|
|
center.className = "aw-ru-dlp-center";
|
|
|
|
|
center.setAttribute("data-aw-ru-dlp-center", "1");
|
|
|
|
|
center.innerHTML =
|
|
|
|
|
'<h4>DLP review и правила</h4>' +
|
|
|
|
|
'<div class="aw-ru-dlp-toolbar">' +
|
|
|
|
|
'<span class="aw-ru-dlp-pill">bucket: ' + escapeHtml(bucketId) + '</span>' +
|
|
|
|
|
'<label><input type="checkbox" data-aw-ru-hide-suppressed /> скрывать события, совпавшие с правилами</label>' +
|
|
|
|
|
'<button type="button" data-aw-ru-refresh-dlp>Обновить DLP</button>' +
|
|
|
|
|
'<div class="aw-ru-dlp-status" data-aw-ru-dlp-status>Событий: 0 · правил: 0 · review: 0</div>' +
|
|
|
|
|
'</div>' +
|
|
|
|
|
'<p>Здесь можно категорировать DLP-события и сохранять suppress/rule записи прямо в AW. Это снижает ложные сработки на уровне review-потока.</p>' +
|
|
|
|
|
'<table class="aw-ru-dlp-table">' +
|
|
|
|
|
'<thead><tr><th>Время</th><th>Тип</th><th>Пользователь</th><th>Документ</th><th>Принтер/канал</th><th>Статус</th><th>Вердикт</th><th>Категория</th><th>Комментарий</th><th>Действия</th></tr></thead>' +
|
|
|
|
|
'<tbody data-aw-ru-dlp-events><tr><td colspan="10">Загрузка...</td></tr></tbody>' +
|
|
|
|
|
'</table>' +
|
|
|
|
|
'<div class="aw-ru-dlp-section">' +
|
|
|
|
|
'<div class="aw-ru-dlp-toolbar">' +
|
|
|
|
|
'<h5>DLP Rules</h5>' +
|
|
|
|
|
'<label><input type="checkbox" data-aw-ru-show-disabled-rules /> показывать отключённые</label>' +
|
|
|
|
|
'</div>' +
|
|
|
|
|
'<table class="aw-ru-dlp-table">' +
|
|
|
|
|
'<thead><tr><th>Время</th><th>Статус</th><th>Действие</th><th>Категория</th><th>Match</th><th>Комментарий</th><th>Управление</th></tr></thead>' +
|
|
|
|
|
'<tbody data-aw-ru-dlp-rules><tr><td colspan="7">Загрузка...</td></tr></tbody>' +
|
|
|
|
|
'</table>' +
|
|
|
|
|
'</div>' +
|
|
|
|
|
'<div class="aw-ru-dlp-section">' +
|
|
|
|
|
'<div class="aw-ru-dlp-toolbar">' +
|
|
|
|
|
'<h5>DLP Review</h5>' +
|
|
|
|
|
'<label><input type="checkbox" data-aw-ru-show-archived-reviews /> показывать архив</label>' +
|
|
|
|
|
'</div>' +
|
|
|
|
|
'<table class="aw-ru-dlp-table">' +
|
|
|
|
|
'<thead><tr><th>Время</th><th>Статус</th><th>Вердикт</th><th>Категория</th><th>Комментарий</th><th>Источник</th><th>Управление</th></tr></thead>' +
|
|
|
|
|
'<tbody data-aw-ru-dlp-reviews><tr><td colspan="7">Загрузка...</td></tr></tbody>' +
|
|
|
|
|
'</table>' +
|
|
|
|
|
'</div>' +
|
|
|
|
|
'<div class="aw-ru-dlp-message" data-aw-ru-dlp-message></div>';
|
|
|
|
|
heading.parentElement.insertBefore(center, heading.nextSibling);
|
|
|
|
|
center.querySelector("[data-aw-ru-refresh-dlp]").addEventListener("click", function () {
|
|
|
|
|
refreshDlpCenter(center, host);
|
|
|
|
|
});
|
|
|
|
|
center.querySelector("[data-aw-ru-hide-suppressed]").addEventListener("change", function () {
|
|
|
|
|
renderDlpTableRows(center, host);
|
|
|
|
|
});
|
|
|
|
|
center.querySelector("[data-aw-ru-show-disabled-rules]").addEventListener("change", function () {
|
|
|
|
|
renderDlpRuleManager(center, host);
|
|
|
|
|
});
|
|
|
|
|
center.querySelector("[data-aw-ru-show-archived-reviews]").addEventListener("change", function () {
|
|
|
|
|
renderDlpReviewManager(center, host);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (center.getAttribute("data-aw-ru-loaded") !== "1") {
|
|
|
|
|
center.setAttribute("data-aw-ru-loaded", "1");
|
|
|
|
|
refreshDlpCenter(center, host);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let trendsRedirectInFlight = false;
|
|
|
|
|
let settingsHostFetchInFlight = false;
|
|
|
|
|
|
|
|
|
|
function getTrendsHostFromSettings(settings) {
|
|
|
|
|
if (!settings || typeof settings !== "object") return "";
|
|
|
|
@@ -347,6 +1000,7 @@
|
|
|
|
|
return response.json();
|
|
|
|
|
})
|
|
|
|
|
.then(function (settings) {
|
|
|
|
|
window.__awRuPatchSettingsHost = getDlpHostFromSettings(settings);
|
|
|
|
|
const host = getTrendsHostFromSettings(settings);
|
|
|
|
|
if (!host || !shouldRedirectTrends(window.location.hash)) return;
|
|
|
|
|
const target = "#/trends/" + encodeURIComponent(host);
|
|
|
|
@@ -360,11 +1014,32 @@
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ensureSettingsHost() {
|
|
|
|
|
if (window.__awRuPatchSettingsHost || settingsHostFetchInFlight) return;
|
|
|
|
|
settingsHostFetchInFlight = true;
|
|
|
|
|
fetch("/api/0/settings/", { credentials: "same-origin" })
|
|
|
|
|
.then(function (response) {
|
|
|
|
|
if (!response.ok) throw new Error("settings-fetch-failed");
|
|
|
|
|
return response.json();
|
|
|
|
|
})
|
|
|
|
|
.then(function (settings) {
|
|
|
|
|
window.__awRuPatchSettingsHost = getDlpHostFromSettings(settings);
|
|
|
|
|
})
|
|
|
|
|
.catch(function () {})
|
|
|
|
|
.finally(function () {
|
|
|
|
|
settingsHostFetchInFlight = false;
|
|
|
|
|
injectDlpNavigation(document.body);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applyPatch() {
|
|
|
|
|
ensureSettingsHost();
|
|
|
|
|
injectStyles();
|
|
|
|
|
walk(document.body);
|
|
|
|
|
translateAttributes(document.body);
|
|
|
|
|
hideNoiseNavigation(document.body);
|
|
|
|
|
injectDlpNavigation(document.body);
|
|
|
|
|
injectDlpReviewCenter(document.body);
|
|
|
|
|
redirectBareTrendsRoute();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|