dtp-tripwire-agent
Version:
A service for protecting your node against malicious attacks and scans.
266 lines • 9.74 kB
JavaScript
// src/service.js
// Copyright (C) 2025 DTP Technologies, LLC
// All Rights Reserved
import env from "./config/env.js";
import path from "node:path";
import fs from "node:fs";
import os from "node:os";
import { readJsonFile } from "./lib/tools.js";
import log from "./lib/log.js";
const UPDATE_INTERVAL = 1000 * 60 * 15;
export var TripwireIncidentType;
(function (TripwireIncidentType) {
TripwireIncidentType["MALFORMED_REQUEST"] = "malformed-request";
TripwireIncidentType["PREFIX_MATCH"] = "uri-prefix-match";
TripwireIncidentType["SUBSTRING_MATCH"] = "uri-substring-match";
TripwireIncidentType["PROHIBITED_METHOD"] = "prohibited-method";
})(TripwireIncidentType || (TripwireIncidentType = {}));
class TripwireService {
cacheDirectory = path.join(env.cache.path, "tripwire");
configuration;
updateTimerId;
async start() {
await this.prepareCacheDirectory();
log.info("starting tripwire service");
await this.loadConfiguration();
log.info("starting tripwire configuration update interval");
this.updateTimerId = setInterval(this.loadConfiguration.bind(this), UPDATE_INTERVAL);
}
async stop() {
if (this.updateTimerId) {
log.info("stopping tripwire configuration update interval");
clearInterval(this.updateTimerId);
delete this.updateTimerId;
}
log.info("tripwire service stopped");
}
middleware() {
return async (req, _res, next) => {
if (!this.configuration) {
log.error("service configuration not loaded");
const error = new Error("Configuration not loaded");
error.name = "TripwireConfigurationError";
error.statusCode = 503;
return next(error);
}
const { method, url } = req;
if (method[0] === "\\" || url[0] === "\\") {
const error = new Error("request is blocked");
error.name = "TripwireRequestBlocked";
error.statusCode = 403;
log.warn(error.message, { method, url });
this.reportIncident(req, {
type: TripwireIncidentType.MALFORMED_REQUEST,
});
return next(error);
}
for (const prefix of this.configuration.prefixes) {
if (url.startsWith(prefix)) {
const error = new Error("request is blocked");
error.statusCode = 403;
log.warn("blocked request", { method, url });
this.reportIncident(req, {
type: TripwireIncidentType.PREFIX_MATCH,
matching: prefix,
});
return next(error);
}
}
for (const text of this.configuration.strings) {
if (url.includes(text)) {
const error = new Error("request is blocked");
error.statusCode = 403;
log.warn("blocked request", { method, url });
this.reportIncident(req, {
type: TripwireIncidentType.SUBSTRING_MATCH,
matching: text,
});
return next(error);
}
}
const blockedMethod = this.configuration.methods.get(method);
if (!blockedMethod) {
return;
}
if (blockedMethod.has(url)) {
const error = new Error(`request is blocked`);
error.statusCode = 403;
log.warn("blocked request", { method, url });
this.reportIncident(req, {
type: TripwireIncidentType.PREFIX_MATCH,
});
return next(error);
}
next();
};
}
async processError(error, req, res) {
let interested = req.url.match(/[admin|user]/gi);
if (!interested) {
return;
}
return this.requestReview(error, req, res);
}
async loadConfiguration() {
let configFile;
try {
configFile = await this.fetchConfiguration();
}
catch (error) {
// fall through
}
try {
if (!configFile) {
configFile = await this.loadConfigurationFile();
}
}
catch (error) {
// fall through
}
if (!configFile) {
const error = new Error("failed to load Tripwire configuration");
error.statusCode = 503;
throw error;
}
this.configuration = {
prefixes: configFile.prefixes,
strings: configFile.strings,
methods: new Map(),
};
for (const entry of configFile.methods) {
this.configuration.methods.set(entry.name, new Set(entry.uris));
}
}
async fetchConfiguration() {
const url = path.join(env.api.url, "configuration");
log.info("loading tripwire configuration", { url });
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${env.api.clientId}:${env.api.clientSecret}`,
},
});
if (!response.ok) {
log.error("failed to receive tripwire service configuration", {
status: response.status,
statusText: response.statusText,
});
return;
}
const json = await response.json();
if (!json.success) {
log.error("failed to receive tripwire service configuration", {
message: json.message,
});
return;
}
log.info("tripwire service configuration updated");
return json.configuration;
}
async loadConfigurationFile() {
const filePath = path.join(this.cacheDirectory, "config.json");
log.info("loading tripwire configuration from cache", { filePath });
try {
const config = await readJsonFile(filePath);
return config;
}
catch (error) {
log.error("failed to load JSON configuration file", { filePath, error });
return;
}
}
async prepareCacheDirectory() {
await fs.promises.mkdir(this.cacheDirectory, { recursive: true });
}
async requestReview(error, req, res) {
const statusCode = error.statusCode || res.statusCode || 500;
const statusMessage = error.message || res.statusMessage || "An error has occured";
log.error("requesting tripwire analysis and review", {
req: {
ip: req.ip,
url: req.url,
},
statusCode,
statusMessage,
error,
});
try {
const report = {
hostname: os.hostname(),
statusCode,
statusMessage,
error,
request: {
ip: req.ip,
ips: req.ips,
host: req.host,
method: req.method,
url: req.url,
originalUrl: req.originalUrl,
protocol: req.protocol,
headers: req.headers,
body: req.body,
},
};
const body = JSON.stringify(report);
const response = await fetch(env.n8n.errorUrl, {
method: "POST",
headers: {
"X-DTP-Tripwire": `${env.n8n.authToken}`,
"Content-Type": "application/json",
"Content-Length": body.length.toString(),
},
body,
});
if (!response.ok) {
const error = new Error("failed to request tripwire review");
error.statusCode = response.status;
throw error;
}
log.info("tripwire analysis request submitted");
}
catch (error) {
log.error("failed to send tripwire event", { error });
}
}
async reportIncident(req, incident) {
try {
const report = {
hostname: os.hostname(),
incident,
request: {
ip: req.ip,
ips: req.ips,
host: req.host,
method: req.method,
url: req.url,
originalUrl: req.originalUrl,
protocol: req.protocol,
headers: req.headers,
body: req.body,
},
};
const body = JSON.stringify(report);
const response = await fetch(env.n8n.eventUrl, {
method: "POST",
headers: {
"X-DTP-Tripwire": `${env.n8n.authToken}`,
"Content-Type": "application/json",
"Content-Length": body.length.toString(),
},
body,
});
if (!response.ok) {
const error = new Error("failed to report tripwire incident");
error.statusCode = response.status;
throw error;
}
log.info("tripwire incident reported");
}
catch (error) {
log.error("failed to send tripwire event", { error });
}
}
}
export default new TripwireService();
//# sourceMappingURL=service.js.map