跳到主要内容

Detect suspicious bulk file changes and temporarily restrict user accounts

This example shows how to detect potentially suspicious user behavior in ONLYOFFICE DocSpace by analyzing audit logs and automatically applying temporary restrictions. The workflow monitors recent file-related activity, identifies users who modify an unusually large number of files in a short time window, sends a security alert, and updates the account status of the affected users.

Before you start

  1. Replace https://yourportal.onlyoffice.com and YOUR_API_KEY with your actual DocSpace portal URL and API key. Ensure you have the necessary data and permissions to perform these operations.
  2. Before you can make requests to the API, you need to authenticate. Check out the Personal access tokens page to learn how to obtain and use access tokens.
Full example
import express from "express";

// =========================
// Config
// =========================

const API_HOST = process.env.DOCSPACE_API_HOST; // Set DOCSPACE_API_HOST in env (recommended). For quick tests you can temporarily paste your portal URL here.
const API_KEY = process.env.DOCSPACE_API_KEY; // Set DOCSPACE_API_KEY in env (recommended). For quick tests you can temporarily paste token here.

// Detection rules
const WINDOW_MINUTES = 10;
const FILE_CHANGE_THRESHOLD = 20;

// Where to notify (placeholder)
const SECURITY_CONTACT = "security-team@yourcompany.com";

const HEADERS: Record<string, string> = {
Accept: "application/json",
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
};

// =========================
// Helpers
// =========================

async function docspaceRequest(path: string, method: string = "GET", body: any = null) {
const url = `${API_HOST}${path}`;

try {
const res = await fetch(url, {
method,
headers: HEADERS,
body: body ? JSON.stringify(body) : undefined,
});

if (!res.ok) {
const text = await res.text().catch(() => "");
console.error(`[ERROR] Request failed: ${method} ${url}`);
console.error(`[ERROR] Status: ${res.status}, Message: ${text}`);
return null;
}

return await res.json();
} catch (err: any) {
console.error(`[ERROR] Request error: ${err?.message || err}`);
return null;
}
}

function toIso(dt: Date) {
return dt.toISOString();
}

// =========================
// Step 1: Load audit events
// =========================

async function getRecentAuditEvents(dtFrom: Date, dtTo: Date) {
const params = new URLSearchParams({
from: toIso(dtFrom),
to: toIso(dtTo),
});

const data = await docspaceRequest(`/api/2.0/security/audit/events/filter?${params.toString()}`, "GET");
const resp = data && typeof data === "object" ? (data.response ?? null) : null;

return Array.isArray(resp) ? resp : [];
}

// =========================
// Step 2: Detect anomalies
// =========================

function isFileChangeEvent(evt: any) {
const entityType = String(evt?.entityType ?? evt?.targetType ?? evt?.entity ?? "").toLowerCase();
const action = String(evt?.action ?? "").toLowerCase();

const isFileEntity = entityType.includes("file") || entityType.includes("document");

const isChangeAction =
action.includes("edit") ||
action.includes("update") ||
action.includes("save") ||
action.includes("modify") ||
action.includes("upload") ||
action.includes("create") ||
action.includes("version");

return isFileEntity && isChangeAction;
}

function buildUserFileChangeMap(events: any[]) {
const result = new Map<string, Set<string>>();

for (const evt of events) {
if (!isFileChangeEvent(evt)) continue;

const userId = String(evt?.userId ?? evt?.account ?? "").trim();
const fileId = String(evt?.entityId ?? evt?.fileId ?? evt?.targetId ?? "").trim();

if (!userId || !fileId) continue;

if (!result.has(userId)) result.set(userId, new Set());
result.get(userId)!.add(fileId);
}

return result;
}

function detectAnomalies(userMap: Map<string, Set<string>>) {
const anomalies: { user_id: string; file_count: number }[] = [];

for (const [userId, fileSet] of userMap.entries()) {
const count = fileSet.size;
if (count > FILE_CHANGE_THRESHOLD) {
anomalies.push({ user_id: userId, file_count: count });
}
}

return anomalies;
}

// =========================
// Step 3: Alert + disable users (Update a user)
// =========================

function sendSecurityAlert(anomalies: { user_id: string; file_count: number }[]) {
console.log("--- SECURITY ALERT ---");
console.log(`Recipient: ${SECURITY_CONTACT}`);
console.log(`Window: last ${WINDOW_MINUTES} minutes`);
console.log(`Threshold: > ${FILE_CHANGE_THRESHOLD} unique files`);
console.log("Suspicious users:");

for (const a of anomalies) {
console.log(`- userId=${a.user_id}, changed_files=${a.file_count}`);
}
}

async function disableUser(userId: string) {
// Update a user
// PUT /api/2.0/people/:userid
// Body includes "disable": true
const payload = {
disable: true,
comment: `Auto-disabled: changed > ${FILE_CHANGE_THRESHOLD} files in ${WINDOW_MINUTES} minutes`,
};

const data = await docspaceRequest(`/api/2.0/people/${encodeURIComponent(userId)}`, "PUT", payload);
return Boolean(data);
}

async function disableSuspiciousUsers(anomalies: { user_id: string; file_count: number }[]) {
if (!anomalies.length) return;

sendSecurityAlert(anomalies);

for (const a of anomalies) {
const ok = await disableUser(a.user_id);
if (ok) console.log(`[OK] User disabled: ${a.user_id}`);
else console.error(`[ERROR] Failed to disable user: ${a.user_id}`);
}
}

// =========================
// Main runner
// =========================

async function runDetection() {
const now = new Date();
const dtFrom = new Date(now.getTime() - WINDOW_MINUTES * 60 * 1000);

console.log(`[INFO] Checking anomalous activity between ${toIso(dtFrom)} and ${toIso(now)}...`);

const events = await getRecentAuditEvents(dtFrom, now);
if (!events.length) {
console.log("[INFO] No audit events in this window.");
return;
}

const userMap = buildUserFileChangeMap(events);
const anomalies = detectAnomalies(userMap);

if (!anomalies.length) {
console.log(`[INFO] No users exceeded the threshold of ${FILE_CHANGE_THRESHOLD} files.`);
return;
}

console.log(`[INFO] Detected ${anomalies.length} user(s) with anomalous activity.`);
await disableSuspiciousUsers(anomalies);

console.log("[INFO] Anomalous activity handling completed.");
}

// =========================
// Optional: expose as an HTTP endpoint (run on demand)
// =========================

const app = express();
app.use(express.json({ limit: "1mb" }));

app.get("/security/scan", async (_req, res) => {
try {
await runDetection();
return res.status(200).json({ status: "ok" });
} catch (e: any) {
console.error("[ERROR] Scan failed:", e?.message || e);
return res.status(500).json({ status: "error" });
}
});

app.listen(3000, () => {
console.log("Security scan endpoint: http://localhost:3000/security/scan");
});

Step 1: Load audit events for a short time window

The script calculates a time range covering the last WINDOW_MINUTES and sends a GET request to /api/2.0/security/audit/events/filter using the from and to query parameters. The response returns audit events recorded during this period.

async function getRecentAuditEvents(dtFrom: Date, dtTo: Date) {
const params = new URLSearchParams({
from: toIso(dtFrom),
to: toIso(dtTo),
});

const data = await docspaceRequest(
`/api/2.0/security/audit/events/filter?${params.toString()}`,
'GET'
);

const resp = data && typeof data === 'object' ? (data.response ?? null) : null;

if (!Array.isArray(resp)) return [];
return resp;
}

Step 2: Detect users with suspicious file-change volume

From the loaded audit events, the script keeps only actions related to file or document changes (for example, edit, update, upload, create, or version events).

For each user:

  • a set of distinct file IDs is collected,
  • the total number of unique files modified by the user is calculated. If the number exceeds FILE_CHANGE_THRESHOLD, the user is marked as suspicious and added to the anomalies list.
function buildUserFileChangeMap(events: any[]) {
const result = new Map<string, Set<string>>();

for (const evt of events) {
if (!isFileChangeEvent(evt)) continue;

const userId = String(evt?.userId ?? evt?.account ?? '');
const fileId = String(evt?.entityId ?? evt?.fileId ?? evt?.targetId ?? '');

if (!userId || !fileId) continue;

if (!result.has(userId)) result.set(userId, new Set());
result.get(userId)!.add(fileId);
}

return result;
}

Step 3: Alert and restrict accounts

If suspicious users are detected:

  1. A security alert is generated (placeholder), containing:
  • user IDs,
  • number of files changed,
  • configured threshold.
  1. A PUT request is sent to /api/2.0/people/:userid with the list of suspicious user IDs. This updates the account status to RESTRICTED_STATUS, temporarily limiting further activity until the incident is reviewed.
function sendSecurityAlert(anomalies: { user_id: string; file_count: number }[]) {
console.log("--- SECURITY ALERT ---");
console.log(`Recipient: ${SECURITY_CONTACT}`);
console.log(`Window: last ${WINDOW_MINUTES} minutes`);
console.log(`Threshold: > ${FILE_CHANGE_THRESHOLD} unique files`);
console.log("Suspicious users:");

for (const a of anomalies) {
console.log(`- userId=${a.user_id}, changed_files=${a.file_count}`);
}
}

async function disableUser(userId: string) {
// Update a user
// PUT /api/2.0/people/:userid
// Body includes "disable": true
const payload = {
disable: true,
comment: `Auto-disabled: changed > ${FILE_CHANGE_THRESHOLD} files in ${WINDOW_MINUTES} minutes`,
};

const data = await docspaceRequest(`/api/2.0/people/${encodeURIComponent(userId)}`, "PUT", payload);
return Boolean(data);
}

async function disableSuspiciousUsers(anomalies: { user_id: string; file_count: number }[]) {
if (!anomalies.length) return;

sendSecurityAlert(anomalies);

for (const a of anomalies) {
const ok = await disableUser(a.user_id);
if (ok) console.log(`[OK] User disabled: ${a.user_id}`);
else console.error(`[ERROR] Failed to disable user: ${a.user_id}`);
}
}