securenow 8.9.0 → 8.11.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/NPM_README.md +30 -0
- package/README.md +142 -360
- package/SKILL-API.md +23 -0
- package/SKILL-CLI.md +21 -3
- package/TELEMETRY-SQL.md +98 -0
- package/cli/agent.js +65 -0
- package/cli/telemetry.js +199 -0
- package/cli.js +56 -1
- package/legacy-semconv.js +349 -0
- package/mcp/catalog.js +85 -0
- package/nextjs.js +773 -768
- package/nuxt-server-plugin.mjs +510 -506
- package/package.json +205 -193
- package/telemetry-schema.d.ts +16 -0
- package/telemetry-schema.js +3 -0
- package/telemetry-schema.json +188 -0
- package/tracing.js +852 -844
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Legacy semantic-convention attributes for the OpenTelemetry 0.221+ /
|
|
5
|
+
* contrib 0.74+ instrumentations, which emit ONLY the stable conventions.
|
|
6
|
+
* OTEL_SEMCONV_STABILITY_OPT_IN no longer brings the old names back.
|
|
7
|
+
*
|
|
8
|
+
* The SecureNow telemetry contract (telemetry-schema.json "automaticTraceFields"),
|
|
9
|
+
* dashboard queries, alert-rule SQL, block/impact reports and false-positive
|
|
10
|
+
* conditions filter on the legacy names. This module restores them next to the
|
|
11
|
+
* stable ones:
|
|
12
|
+
*
|
|
13
|
+
* - HTTP (withLegacyHttpSemconv): http.method, http.target, http.url,
|
|
14
|
+
* http.status_code, http.host, http.scheme, http.user_agent, net.peer.ip, ...
|
|
15
|
+
* via the HttpInstrumentation's public hooks, reproducing what
|
|
16
|
+
* instrumentation-http <= 0.220 emitted by default.
|
|
17
|
+
* - Databases (withLegacyDbSemconvExporter): db.statement, db.system, db.name,
|
|
18
|
+
* db.operation, db.mongodb.collection / db.sql.table, net.peer.name/port,
|
|
19
|
+
* derived at export time from db.query.text, db.system.name, db.namespace,
|
|
20
|
+
* db.operation.name, db.collection.name, server.address/port. The values are
|
|
21
|
+
* the same ones the old instrumentations wrote under both names.
|
|
22
|
+
*
|
|
23
|
+
* Deliberate HTTP differences from <= 0.220:
|
|
24
|
+
* - http.client_ip is NOT derived from the first X-Forwarded-For entry (that
|
|
25
|
+
* value is spoofable). SecureNow's requestHook sets http.client_ip from the
|
|
26
|
+
* trusted-proxy resolver (resolve-ip.js).
|
|
27
|
+
* - Signature-style query parameters (same list upstream now redacts on
|
|
28
|
+
* url.query / url.full) are replaced with REDACTED in http.target / http.url.
|
|
29
|
+
* Every other byte of the target is kept exactly as received, so detection
|
|
30
|
+
* rules that match encoded payloads keep working.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
// Mirrors DEFAULT_QUERY_STRINGS_TO_REDACT in @opentelemetry/instrumentation-http.
|
|
34
|
+
const DEFAULT_REDACTED_QUERY_PARAMS = Object.freeze([
|
|
35
|
+
'sig',
|
|
36
|
+
'Signature',
|
|
37
|
+
'AWSAccessKeyId',
|
|
38
|
+
'X-Goog-Signature',
|
|
39
|
+
'X-Amz-Signature',
|
|
40
|
+
'X-Amz-Credential',
|
|
41
|
+
'X-Amz-Security-Token',
|
|
42
|
+
]);
|
|
43
|
+
const REDACTED = 'REDACTED';
|
|
44
|
+
|
|
45
|
+
function isServerResponse(value) {
|
|
46
|
+
return !!value && typeof value.setHeader === 'function' && typeof value.writeHead === 'function';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function defined(attributes) {
|
|
50
|
+
const out = {};
|
|
51
|
+
for (const key of Object.keys(attributes)) {
|
|
52
|
+
if (attributes[key] !== undefined && attributes[key] !== null) out[key] = attributes[key];
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Redact the values of sensitive query parameters in a `?query` string while
|
|
59
|
+
* leaving every other byte untouched (no re-encoding). Returns the input when
|
|
60
|
+
* nothing matched.
|
|
61
|
+
*/
|
|
62
|
+
function redactSearch(search, paramsToRedact = DEFAULT_REDACTED_QUERY_PARAMS) {
|
|
63
|
+
if (typeof search !== 'string' || search.length < 2 || search[0] !== '?') return search;
|
|
64
|
+
const sensitive = new Set(paramsToRedact || []);
|
|
65
|
+
if (sensitive.size === 0) return search;
|
|
66
|
+
let changed = false;
|
|
67
|
+
const parts = search.slice(1).split('&').map((part) => {
|
|
68
|
+
const eq = part.indexOf('=');
|
|
69
|
+
const rawKey = eq === -1 ? part : part.slice(0, eq);
|
|
70
|
+
let key = rawKey;
|
|
71
|
+
try {
|
|
72
|
+
key = decodeURIComponent(rawKey.replace(/\+/g, ' '));
|
|
73
|
+
} catch (_) {}
|
|
74
|
+
if (!sensitive.has(key)) return part;
|
|
75
|
+
changed = true;
|
|
76
|
+
return `${rawKey}=${REDACTED}`;
|
|
77
|
+
});
|
|
78
|
+
return changed ? `?${parts.join('&')}` : search;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function contentLengthAttributes(headers, direction) {
|
|
82
|
+
const raw = headers && headers['content-length'];
|
|
83
|
+
if (raw === undefined) return {};
|
|
84
|
+
const length = parseInt(raw, 10);
|
|
85
|
+
if (Number.isNaN(length)) return {};
|
|
86
|
+
const encoding = headers['content-encoding'];
|
|
87
|
+
const compressed = !!encoding && encoding !== 'identity';
|
|
88
|
+
return compressed
|
|
89
|
+
? { [`http.${direction}_content_length`]: length }
|
|
90
|
+
: { [`http.${direction}_content_length_uncompressed`]: length };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function httpKindAttributes(httpVersion) {
|
|
94
|
+
if (!httpVersion) return {};
|
|
95
|
+
return {
|
|
96
|
+
'http.flavor': httpVersion,
|
|
97
|
+
'net.transport': String(httpVersion).toUpperCase() !== 'QUIC' ? 'ip_tcp' : 'ip_udp',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---- incoming (SERVER spans) ----
|
|
102
|
+
|
|
103
|
+
function incomingRequestAttributes(request, paramsToRedact) {
|
|
104
|
+
const headers = request.headers || {};
|
|
105
|
+
const component = request.socket && request.socket.encrypted ? 'https' : 'http';
|
|
106
|
+
const host = headers.host;
|
|
107
|
+
const hostname = (typeof host === 'string' && host.replace(/^(.*)(:[0-9]{1,5})/, '$1')) || 'localhost';
|
|
108
|
+
const userAgent = headers['user-agent'];
|
|
109
|
+
|
|
110
|
+
let pathname;
|
|
111
|
+
let search = '';
|
|
112
|
+
let fullUrl;
|
|
113
|
+
try {
|
|
114
|
+
if (host) {
|
|
115
|
+
const parsed = new URL(request.url || '/', `${component}://${host}`);
|
|
116
|
+
pathname = parsed.pathname;
|
|
117
|
+
const redacted = redactSearch(parsed.search, paramsToRedact);
|
|
118
|
+
if (redacted !== parsed.search) parsed.search = redacted;
|
|
119
|
+
search = parsed.search;
|
|
120
|
+
fullUrl = parsed.toString();
|
|
121
|
+
} else {
|
|
122
|
+
const parsed = new URL(request.url || '/', `${component}://localhost`);
|
|
123
|
+
pathname = parsed.pathname;
|
|
124
|
+
search = redactSearch(parsed.search, paramsToRedact);
|
|
125
|
+
fullUrl = pathname + search;
|
|
126
|
+
}
|
|
127
|
+
} catch (_) {}
|
|
128
|
+
|
|
129
|
+
const attributes = {
|
|
130
|
+
'http.url': fullUrl,
|
|
131
|
+
'http.host': host,
|
|
132
|
+
'net.host.name': hostname,
|
|
133
|
+
'http.method': request.method,
|
|
134
|
+
'http.scheme': component,
|
|
135
|
+
...contentLengthAttributes(headers, 'request'),
|
|
136
|
+
...httpKindAttributes(request.httpVersion),
|
|
137
|
+
};
|
|
138
|
+
if (pathname) attributes['http.target'] = pathname + search || '/';
|
|
139
|
+
if (userAgent !== undefined) attributes['http.user_agent'] = userAgent;
|
|
140
|
+
return defined(attributes);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function incomingResponseAttributes(request, response) {
|
|
144
|
+
const socket = request && request.socket;
|
|
145
|
+
const attributes = {
|
|
146
|
+
'http.status_code': response.statusCode,
|
|
147
|
+
'http.status_text': String(response.statusMessage || '').toUpperCase(),
|
|
148
|
+
};
|
|
149
|
+
if (socket) {
|
|
150
|
+
attributes['net.host.ip'] = socket.localAddress;
|
|
151
|
+
attributes['net.host.port'] = socket.localPort;
|
|
152
|
+
attributes['net.peer.ip'] = socket.remoteAddress;
|
|
153
|
+
attributes['net.peer.port'] = socket.remotePort;
|
|
154
|
+
}
|
|
155
|
+
return defined(attributes);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---- outgoing (CLIENT spans) ----
|
|
159
|
+
|
|
160
|
+
function hostnameAndPort(options) {
|
|
161
|
+
if (options.hostname && options.port) return { hostname: options.hostname, port: options.port };
|
|
162
|
+
const matches = (typeof options.host === 'string' && options.host.match(/^([^:/ ]+)(:\d{1,5})?/)) || null;
|
|
163
|
+
const hostname = options.hostname || (matches === null ? 'localhost' : matches[1]);
|
|
164
|
+
let port = options.port;
|
|
165
|
+
if (!port) {
|
|
166
|
+
if (matches && matches[2]) port = matches[2].substring(1);
|
|
167
|
+
else port = options.protocol === 'https:' ? '443' : '80';
|
|
168
|
+
}
|
|
169
|
+
return { hostname, port };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function absoluteUrl(options, headers, paramsToRedact) {
|
|
173
|
+
const protocol = options.protocol || 'http:';
|
|
174
|
+
const port = String(options.port || '');
|
|
175
|
+
let path = options.path || '/';
|
|
176
|
+
let host = options.host || options.hostname || headers.host || 'localhost';
|
|
177
|
+
if (String(host).indexOf(':') === -1 && port && port !== '80' && port !== '443') host += `:${port}`;
|
|
178
|
+
if (path.includes('?')) {
|
|
179
|
+
// Same WHATWG normalization as instrumentation-http <= 0.220.
|
|
180
|
+
try {
|
|
181
|
+
const parsed = new URL(path, 'http://localhost');
|
|
182
|
+
path = `${parsed.pathname}${redactSearch(parsed.search, paramsToRedact)}`;
|
|
183
|
+
} catch (_) {}
|
|
184
|
+
}
|
|
185
|
+
const auth = options.auth ? `${REDACTED}:${REDACTED}@` : '';
|
|
186
|
+
return `${protocol}//${auth}${host}${path}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function outgoingRequestAttributes(options, paramsToRedact) {
|
|
190
|
+
const headers = options.headers || {};
|
|
191
|
+
const { hostname, port } = hostnameAndPort(options);
|
|
192
|
+
const rawPath = options.path || '/';
|
|
193
|
+
const q = rawPath.indexOf('?');
|
|
194
|
+
const target = q === -1 ? rawPath : rawPath.slice(0, q) + redactSearch(rawPath.slice(q), paramsToRedact);
|
|
195
|
+
const attributes = {
|
|
196
|
+
'http.url': absoluteUrl(options, headers, paramsToRedact),
|
|
197
|
+
'http.method': options.method || 'GET',
|
|
198
|
+
'http.target': target,
|
|
199
|
+
'net.peer.name': hostname,
|
|
200
|
+
'http.host': headers.host !== undefined ? headers.host : `${hostname}:${port}`,
|
|
201
|
+
};
|
|
202
|
+
if (headers['user-agent'] !== undefined) attributes['http.user_agent'] = headers['user-agent'];
|
|
203
|
+
return defined(attributes);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function outgoingResponseAttributes(response) {
|
|
207
|
+
const { statusCode, statusMessage, httpVersion, socket } = response;
|
|
208
|
+
const attributes = {
|
|
209
|
+
...contentLengthAttributes(response.headers || {}, 'response'),
|
|
210
|
+
...httpKindAttributes(httpVersion),
|
|
211
|
+
};
|
|
212
|
+
if (socket) {
|
|
213
|
+
attributes['net.peer.ip'] = socket.remoteAddress;
|
|
214
|
+
attributes['net.peer.port'] = socket.remotePort;
|
|
215
|
+
}
|
|
216
|
+
if (statusCode) {
|
|
217
|
+
attributes['http.status_code'] = statusCode;
|
|
218
|
+
attributes['http.status_text'] = String(statusMessage || '').toUpperCase();
|
|
219
|
+
}
|
|
220
|
+
return defined(attributes);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function safe(fn, fallback) {
|
|
224
|
+
try {
|
|
225
|
+
return fn();
|
|
226
|
+
} catch (_) {
|
|
227
|
+
return fallback;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Returns a copy of an HttpInstrumentation config whose hooks also emit the
|
|
233
|
+
* legacy attributes. Existing hooks are preserved and run after the legacy
|
|
234
|
+
* attributes are set (same ordering as instrumentation-http <= 0.220, so a
|
|
235
|
+
* caller hook can still override a legacy value), except for the final
|
|
236
|
+
* server-side status which, like <= 0.220, is written when the response ends.
|
|
237
|
+
*/
|
|
238
|
+
function withLegacyHttpSemconv(config = {}) {
|
|
239
|
+
const serverParams = config.redactedQueryParamsServer || DEFAULT_REDACTED_QUERY_PARAMS;
|
|
240
|
+
const clientParams = config.redactedQueryParams || DEFAULT_REDACTED_QUERY_PARAMS;
|
|
241
|
+
const {
|
|
242
|
+
startIncomingSpanHook,
|
|
243
|
+
startOutgoingSpanHook,
|
|
244
|
+
responseHook,
|
|
245
|
+
applyCustomAttributesOnSpan,
|
|
246
|
+
} = config;
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
...config,
|
|
250
|
+
startIncomingSpanHook(request) {
|
|
251
|
+
const legacy = safe(() => incomingRequestAttributes(request, serverParams), {});
|
|
252
|
+
const custom = typeof startIncomingSpanHook === 'function' ? startIncomingSpanHook(request) : undefined;
|
|
253
|
+
return { ...legacy, ...(custom || {}) };
|
|
254
|
+
},
|
|
255
|
+
startOutgoingSpanHook(options) {
|
|
256
|
+
const legacy = safe(() => outgoingRequestAttributes(options, clientParams), {});
|
|
257
|
+
const custom = typeof startOutgoingSpanHook === 'function' ? startOutgoingSpanHook(options) : undefined;
|
|
258
|
+
return { ...legacy, ...(custom || {}) };
|
|
259
|
+
},
|
|
260
|
+
responseHook(span, response) {
|
|
261
|
+
// CLIENT spans receive the IncomingMessage here once the response
|
|
262
|
+
// arrives. SERVER spans receive the ServerResponse before the handler
|
|
263
|
+
// runs; their legacy status is set in applyCustomAttributesOnSpan.
|
|
264
|
+
if (!isServerResponse(response)) {
|
|
265
|
+
safe(() => span.setAttributes(outgoingResponseAttributes(response)));
|
|
266
|
+
}
|
|
267
|
+
if (typeof responseHook === 'function') responseHook(span, response);
|
|
268
|
+
},
|
|
269
|
+
applyCustomAttributesOnSpan(span, request, response) {
|
|
270
|
+
if (isServerResponse(response)) {
|
|
271
|
+
safe(() => span.setAttributes(incomingResponseAttributes(request, response)));
|
|
272
|
+
}
|
|
273
|
+
if (typeof applyCustomAttributesOnSpan === 'function') applyCustomAttributesOnSpan(span, request, response);
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ---- databases (contrib instrumentations >= 0.74) ----
|
|
279
|
+
|
|
280
|
+
// Stable db.system.name values that differ from the legacy db.system values.
|
|
281
|
+
const LEGACY_DB_SYSTEM = Object.freeze({
|
|
282
|
+
'microsoft.sql_server': 'mssql',
|
|
283
|
+
'oracle.db': 'oracle',
|
|
284
|
+
});
|
|
285
|
+
const SQL_SYSTEMS = new Set(['mssql', 'microsoft.sql_server']);
|
|
286
|
+
// instrumentation-mongoose 0.64/0.66 (securenow 8.9/8.10) wrote db.system="mongoose";
|
|
287
|
+
// 0.68 only reports db.system.name="mongodb". Keep "mongoose" so `db.system = 'mongodb'` queries don't
|
|
288
|
+
// start double-counting each operation (mongoose span + driver span).
|
|
289
|
+
const LEGACY_DB_SYSTEM_BY_SCOPE = Object.freeze({
|
|
290
|
+
'@opentelemetry/instrumentation-mongoose': 'mongoose',
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
function legacyDbAttributes(attributes, scopeName) {
|
|
294
|
+
if (!attributes || attributes['db.system.name'] === undefined) return null;
|
|
295
|
+
const system = attributes['db.system.name'];
|
|
296
|
+
const legacy = {};
|
|
297
|
+
const alias = (from, to, map) => {
|
|
298
|
+
if (attributes[from] === undefined || attributes[to] !== undefined) return;
|
|
299
|
+
legacy[to] = map ? map(attributes[from]) : attributes[from];
|
|
300
|
+
};
|
|
301
|
+
alias('db.system.name', 'db.system', (v) => LEGACY_DB_SYSTEM_BY_SCOPE[scopeName] || LEGACY_DB_SYSTEM[v] || v);
|
|
302
|
+
alias('db.query.text', 'db.statement');
|
|
303
|
+
alias('db.namespace', 'db.name');
|
|
304
|
+
alias('db.operation.name', 'db.operation');
|
|
305
|
+
alias('server.address', 'net.peer.name');
|
|
306
|
+
alias('server.port', 'net.peer.port');
|
|
307
|
+
if (system === 'mongodb') alias('db.collection.name', 'db.mongodb.collection');
|
|
308
|
+
else if (SQL_SYSTEMS.has(system)) alias('db.collection.name', 'db.sql.table');
|
|
309
|
+
return Object.keys(legacy).length ? legacy : null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Wraps a SpanExporter so database client spans are exported with the legacy
|
|
314
|
+
* db.* aliases as well. The finished span is not mutated: the exporter gets a
|
|
315
|
+
* view that delegates to it and only overrides `attributes`.
|
|
316
|
+
*/
|
|
317
|
+
function withLegacyDbSemconvExporter(exporter) {
|
|
318
|
+
const view = (span) => {
|
|
319
|
+
const legacy = safe(() => legacyDbAttributes(span.attributes, span.instrumentationScope && span.instrumentationScope.name), null);
|
|
320
|
+
if (!legacy) return span;
|
|
321
|
+
return Object.create(span, {
|
|
322
|
+
attributes: { value: { ...span.attributes, ...legacy }, enumerable: true },
|
|
323
|
+
});
|
|
324
|
+
};
|
|
325
|
+
return {
|
|
326
|
+
export(spans, resultCallback) {
|
|
327
|
+
const mapped = safe(() => spans.map(view), spans);
|
|
328
|
+
return exporter.export(mapped, resultCallback);
|
|
329
|
+
},
|
|
330
|
+
shutdown() {
|
|
331
|
+
return exporter.shutdown();
|
|
332
|
+
},
|
|
333
|
+
forceFlush() {
|
|
334
|
+
return typeof exporter.forceFlush === 'function' ? exporter.forceFlush() : Promise.resolve();
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
module.exports = {
|
|
340
|
+
DEFAULT_REDACTED_QUERY_PARAMS,
|
|
341
|
+
withLegacyHttpSemconv,
|
|
342
|
+
withLegacyDbSemconvExporter,
|
|
343
|
+
legacyDbAttributes,
|
|
344
|
+
redactSearch,
|
|
345
|
+
incomingRequestAttributes,
|
|
346
|
+
incomingResponseAttributes,
|
|
347
|
+
outgoingRequestAttributes,
|
|
348
|
+
outgoingResponseAttributes,
|
|
349
|
+
};
|
package/mcp/catalog.js
CHANGED
|
@@ -1936,6 +1936,26 @@ const PROMPTS = [
|
|
|
1936
1936
|
{ name: 'confirmWrites', description: 'Set true only when the user explicitly wants the MCP agent to execute decisions.', required: false },
|
|
1937
1937
|
],
|
|
1938
1938
|
},
|
|
1939
|
+
{
|
|
1940
|
+
name: 'work_alert_review',
|
|
1941
|
+
title: 'Work Alert Review',
|
|
1942
|
+
description: 'Use MCP tools to clear the Alert Review queue from root-cause buckets: tune noisy rules with validated guards, batch-resolve covered rows, and keep real detections alive.',
|
|
1943
|
+
arguments: [
|
|
1944
|
+
{ name: 'limit', description: 'Maximum root-cause groups to work this run. Defaults to 10.', required: false },
|
|
1945
|
+
{ name: 'search', description: 'Optional search filter over rule, route, IP, or evidence.', required: false },
|
|
1946
|
+
{ name: 'confirmWrites', description: 'Set true only when the user explicitly wants the MCP agent to execute decisions.', required: false },
|
|
1947
|
+
],
|
|
1948
|
+
},
|
|
1949
|
+
{
|
|
1950
|
+
name: 'soc_patrol',
|
|
1951
|
+
title: 'SOC Patrol (Scheduled)',
|
|
1952
|
+
description: 'The recurring SOC pass SecureNow\'s built-in agent used to run on a 5-minute cron. Run it from your own harness (Claude, Hermes, OpenClaw, ...) on a schedule: Alert Review root-cause fixes first, then the Requires Human queue.',
|
|
1953
|
+
arguments: [
|
|
1954
|
+
{ name: 'maxAlertReviewGroups', description: 'Maximum alert-review root-cause groups to work this pass. Defaults to 10.', required: false },
|
|
1955
|
+
{ name: 'maxHumanRows', description: 'Maximum Requires Human rows to work this pass. Defaults to 10.', required: false },
|
|
1956
|
+
{ name: 'confirmWrites', description: 'Set true only when this scheduled run is allowed to execute decisions autonomously.', required: false },
|
|
1957
|
+
],
|
|
1958
|
+
},
|
|
1939
1959
|
{
|
|
1940
1960
|
name: 'cleanup_legacy_pending_blocks',
|
|
1941
1961
|
title: 'Clean Legacy Pending Blocks',
|
|
@@ -2079,6 +2099,71 @@ function promptMessages(name, args = {}) {
|
|
|
2079
2099
|
];
|
|
2080
2100
|
}
|
|
2081
2101
|
|
|
2102
|
+
if (name === 'work_alert_review') {
|
|
2103
|
+
const limit = args.limit || 10;
|
|
2104
|
+
const confirmWrites = args.confirmWrites === true || args.confirmWrites === 'true';
|
|
2105
|
+
return [
|
|
2106
|
+
{
|
|
2107
|
+
role: 'user',
|
|
2108
|
+
content: {
|
|
2109
|
+
type: 'text',
|
|
2110
|
+
text: [
|
|
2111
|
+
'Work my SecureNow Alert Review queue like a senior detection engineer using the MCP tools.',
|
|
2112
|
+
`Work up to ${limit} root-cause group(s), biggest and most urgent first.${args.search ? ` Search filter: ${args.search}.` : ''}`,
|
|
2113
|
+
'Start with securenow_alert_review_runs_count, then securenow_alert_review_runs_list to get the root-cause groups. Work buckets before singletons: one upstream rule fix closes many downstream rows.',
|
|
2114
|
+
'For each group, inspect a prototype row with securenow_alert_review_run_get and preview the sibling rows with securenow_alert_review_group_similar. Pull trace evidence with securenow_traces_show / securenow_logs_for_trace when trace IDs are available.',
|
|
2115
|
+
'Classify the root cause: (a) benign false-positive pattern -> tune the rule, (b) real detection that fired correctly -> keep the rule alive and close the review rows, (c) mixed or unclear -> leave open with the exact missing proof.',
|
|
2116
|
+
'For rule tuning, propose a narrow attack-preserving guard: combine at least TWO of path, method, status_code, user_agent. Never guard on attacker-controllable fields alone (request body/header payloads), never use path starts_with "/" or catch-all regex. Validate with securenow_alert_rule_condition_candidate_test and securenow_alert_rule_condition_diff (or a dry run) BEFORE applying; the guard must suppress the benign samples while attack examples still match.',
|
|
2117
|
+
'Apply validated tuning with securenow_alert_rule_condition_update or securenow_alert_rule_exclusion_add. If the rule is a system/global rule and the tool returns access denied, do not force a customer-specific workaround — record the exact prepared guard in the decision report with outcome=rule_tuning and the missing permission.',
|
|
2118
|
+
'After a rule fix, close the covered rows with securenow_alert_review_run_apply_group_decision (use previewOnly:true first; set allowMixed only after reviewing every row). For correct detections use securenow_alert_review_run_keep_alive. For one-off rows use securenow_alert_review_run_resolve or securenow_alert_review_run_dismiss with a full decision report.',
|
|
2119
|
+
confirmWrites
|
|
2120
|
+
? 'The user requested execution. Call write tools with confirm:true, a precise reason, and a decisionReport containing summary, evidence, reviewedHistory, traceIds, and missingProof when relevant.'
|
|
2121
|
+
: 'Do not execute write tools yet. Produce a group-by-group plan with the exact validated guards and MCP write calls for user approval.',
|
|
2122
|
+
'Never resolve a group as false positive without concrete benign samples AND proof the guard preserves attack detection. When uncertain, keep the rule alive and leave the row open with the missing proof named.',
|
|
2123
|
+
'End with counts: groups worked, rules tuned (with the exact guards), rows resolved/dismissed/kept alive, rows left open with missing proof, and any permission blockers.',
|
|
2124
|
+
].join('\n'),
|
|
2125
|
+
},
|
|
2126
|
+
},
|
|
2127
|
+
];
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
if (name === 'soc_patrol') {
|
|
2131
|
+
const maxAlertReviewGroups = args.maxAlertReviewGroups || 10;
|
|
2132
|
+
const maxHumanRows = args.maxHumanRows || 10;
|
|
2133
|
+
const confirmWrites = args.confirmWrites === true || args.confirmWrites === 'true';
|
|
2134
|
+
return [
|
|
2135
|
+
{
|
|
2136
|
+
role: 'user',
|
|
2137
|
+
content: {
|
|
2138
|
+
type: 'text',
|
|
2139
|
+
text: [
|
|
2140
|
+
'Run one scheduled SecureNow SOC patrol pass using the MCP tools. This is the recurring pass the built-in SecureNow agent used to run every 5 minutes; you are its external replacement, so be conservative, auditable, and finish in one bounded pass.',
|
|
2141
|
+
'',
|
|
2142
|
+
'Phase 0 — check for work:',
|
|
2143
|
+
'- Call securenow_alert_review_runs_count and securenow_human_actions_list (page=1, limit=1).',
|
|
2144
|
+
'- If both queues are empty, stop and report "no open SOC work".',
|
|
2145
|
+
'',
|
|
2146
|
+
'Phase 1 — Alert Review first (root-cause rule fixes are the upstream fix; they auto-resolve downstream human work):',
|
|
2147
|
+
`- Follow the work_alert_review runbook for up to ${maxAlertReviewGroups} root-cause group(s): list groups, inspect prototypes and siblings, validate narrow attack-preserving guards (two or more of path/method/status_code/user_agent) with condition tests/diffs before applying, tune rules, batch-resolve covered rows, keep real detections alive, and leave unclear rows open with the exact missing proof.`,
|
|
2148
|
+
'',
|
|
2149
|
+
'Phase 2 — Requires Human queue:',
|
|
2150
|
+
`- Follow the work_human_actions runbook for up to ${maxHumanRows} row(s): list the queue most urgent first, read each AI report and trace evidence, then choose exactly one outcome per row (block, rate limit, scoped false positive, rule tuning, case action approve/reject, or an auditable no-write decision report with the missing proof).`,
|
|
2151
|
+
'- Re-check rows against any rule changes from Phase 1 before deciding — a Phase 1 fix often already covers them.',
|
|
2152
|
+
'',
|
|
2153
|
+
'Safety (this run may be unattended):',
|
|
2154
|
+
confirmWrites
|
|
2155
|
+
? '- Writes are allowed this pass: call write tools with confirm:true, a precise reason, and a full decisionReport. Never block without high-confidence malicious evidence (riskScore >= 90 or equivalent proof). Never create broad false positives or single-condition exclusions. Prefer an auditable no-write decision report whenever evidence is mixed.'
|
|
2156
|
+
: '- Read-only pass: do NOT call write tools. Produce the exact prepared write calls (with guards and reasons) as a reviewable plan.',
|
|
2157
|
+
'- Never trust an IP globally, never create IP allowlist entries, never suppress on attacker-controllable fields alone.',
|
|
2158
|
+
'- Stay within the row/group budgets; leave the rest for the next scheduled pass.',
|
|
2159
|
+
'',
|
|
2160
|
+
'End with a compact patrol report: queues checked, groups/rows worked, rules tuned (exact guards), blocks/rate limits/false positives executed or prepared, rows left open with missing proof, and anything that needs a human decision.',
|
|
2161
|
+
].join('\n'),
|
|
2162
|
+
},
|
|
2163
|
+
},
|
|
2164
|
+
];
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2082
2167
|
if (name === 'cleanup_legacy_pending_blocks') {
|
|
2083
2168
|
const limit = args.limit || 50;
|
|
2084
2169
|
const environment = args.environment || 'production';
|