securenow 8.14.1 → 8.15.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/README.md +2 -0
- package/SKILL-CLI.md +11 -1
- package/cli/security.js +194 -54
- package/cli.js +2 -1
- package/mcp/catalog.js +88 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -163,6 +163,8 @@ Everything the MCP server can do is also a command. Run `npx securenow help` for
|
|
|
163
163
|
| `securenow ip <address>` | IP intelligence (geo, abuse, verdict) |
|
|
164
164
|
| `securenow ip traces <address>` | Traces from an IP |
|
|
165
165
|
| `securenow forensics "<query>"` | Natural-language forensic query |
|
|
166
|
+
| `securenow forensics chat --app <key> ["<question>"]` | Forensics chat with memory; `--conversation <id>` continues one |
|
|
167
|
+
| `securenow forensics conversations [<id>]` | List forensics conversations, or show one |
|
|
166
168
|
| `securenow traces` / `traces show <traceId>` | Recent traces / trace spans |
|
|
167
169
|
| `securenow traces analyze <traceId>` | AI security analysis of a trace |
|
|
168
170
|
| `securenow logs [--since 6h] [--level error]` | View logs |
|
package/SKILL-CLI.md
CHANGED
|
@@ -334,10 +334,20 @@ securenow ip traces <ip-address> --env production # traces originating from this
|
|
|
334
334
|
```bash
|
|
335
335
|
securenow forensics "show me all SQL injection attempts in the last 24h" --env production
|
|
336
336
|
securenow forensics query "top 10 IPs by blocked requests" --app my-app --env production
|
|
337
|
-
securenow forensics chat --app my-app --env production # interactive forensics chat session
|
|
337
|
+
securenow forensics chat --app my-app --env production # interactive forensics chat session (with memory)
|
|
338
|
+
securenow forensics chat --app my-app "which accounts had failed logins then a success?" # one question, prints the conversation id
|
|
339
|
+
securenow forensics chat --conversation <id> "what else did those IPs do?" # follow up: the chat remembers the earlier answers
|
|
340
|
+
securenow forensics conversations # list conversations (dashboard, CLI and agent ones)
|
|
341
|
+
securenow forensics conversations <id> # show one: questions, SQL, findings, rows
|
|
338
342
|
securenow forensics library # view saved/template queries
|
|
339
343
|
```
|
|
340
344
|
|
|
345
|
+
The chat runs discovery queries before answering, shows its SQL, and ends each answer with findings: what the result rows
|
|
346
|
+
show. A follow-up sees the earlier questions, their findings, and the IPs and trace IDs in their results. MCP:
|
|
347
|
+
`securenow_forensics_chat` (pass `conversationId` to continue), `securenow_forensics_conversations_list`,
|
|
348
|
+
`securenow_forensics_conversation_get`. `securenow forensics "<q>"` and `securenow_forensics_query` answer one question
|
|
349
|
+
with no memory.
|
|
350
|
+
|
|
341
351
|
---
|
|
342
352
|
|
|
343
353
|
### Firewall
|
package/cli/security.js
CHANGED
|
@@ -1509,24 +1509,124 @@ async function forensicsLibrary(args, flags) {
|
|
|
1509
1509
|
|
|
1510
1510
|
// ── Forensics Chat ──
|
|
1511
1511
|
|
|
1512
|
-
|
|
1513
|
-
|
|
1512
|
+
const CHAT_PRINT_ROWS = 25;
|
|
1513
|
+
|
|
1514
|
+
// Log timestamps come back as nanosecond (or microsecond) epochs when a query returns
|
|
1515
|
+
// min(timestamp) and the like from the logs table; print them as times.
|
|
1516
|
+
function chatCell(value) {
|
|
1517
|
+
const s = String(value ?? '');
|
|
1518
|
+
if (/^\d{16,19}$/.test(s)) {
|
|
1519
|
+
const n = Number(s);
|
|
1520
|
+
const ms = n >= 1e18 && n < 4e18 ? n / 1e6 : n >= 1e15 && n < 4e15 ? n / 1e3 : null;
|
|
1521
|
+
if (ms != null) return new Date(ms).toISOString().replace('T', ' ').replace('Z', '');
|
|
1522
|
+
}
|
|
1523
|
+
return typeof value === 'object' && value !== null ? JSON.stringify(value) : s;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// The API answers a chat message with 202 and works in the background; poll until
|
|
1527
|
+
// the reply settles (complete or awaiting_confirmation) or about five minutes pass.
|
|
1528
|
+
async function waitForChatReply(conversationId, environment, s) {
|
|
1529
|
+
for (let i = 0; i < 150; i++) {
|
|
1530
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
1531
|
+
const result = await api.get(`/forensics/chat/status/${conversationId}`, { query: { environment } });
|
|
1532
|
+
if (result.status !== 'processing') return { ...result, conversationId };
|
|
1533
|
+
if (result._progress?.action && s) s.update(result._progress.action);
|
|
1534
|
+
}
|
|
1535
|
+
return { conversationId, status: 'processing' };
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
async function sendChatMessage(body, environment, s) {
|
|
1539
|
+
const chatRes = await api.post('/forensics/chat', body);
|
|
1540
|
+
if (chatRes.status !== 'processing') {
|
|
1541
|
+
return { conversationId: chatRes.conversationId, status: 'complete', message: chatRes.message };
|
|
1542
|
+
}
|
|
1543
|
+
return waitForChatReply(chatRes.conversationId, environment, s);
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
async function confirmChatQuery(conversationId, environment, s) {
|
|
1547
|
+
await api.post(`/forensics/chat/confirm/${conversationId}`, { environment });
|
|
1548
|
+
return waitForChatReply(conversationId, environment, s);
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// Resolve what a chat talks to: an existing conversation (--conversation <id>,
|
|
1552
|
+
// which carries its own app and environment) or a new one scoped to --app.
|
|
1553
|
+
async function resolveChatTarget(flags) {
|
|
1554
|
+
if (typeof flags.conversation === 'string' && flags.conversation) {
|
|
1555
|
+
const data = await api.get(`/forensics/conversations/${flags.conversation}`);
|
|
1556
|
+
const conv = data.conversation;
|
|
1557
|
+
return {
|
|
1558
|
+
conversationId: conv.id,
|
|
1559
|
+
title: conv.title,
|
|
1560
|
+
label: conv.applicationName || conv.applicationKey || conv.applicationId,
|
|
1561
|
+
appKey: conv.applicationKey,
|
|
1562
|
+
environment: conv.environment || 'production',
|
|
1563
|
+
messageCount: (conv.messages || []).length,
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1514
1566
|
const resolved = await resolveAppId(flags);
|
|
1515
1567
|
if (!resolved) {
|
|
1516
|
-
ui.error('App required. Usage: securenow forensics chat --app <key>');
|
|
1568
|
+
ui.error('App required. Usage: securenow forensics chat --app <key>, or --conversation <id> to continue one.');
|
|
1517
1569
|
ui.info('Set a default with: securenow apps default <key>');
|
|
1518
1570
|
process.exit(1);
|
|
1519
1571
|
}
|
|
1572
|
+
return {
|
|
1573
|
+
conversationId: null,
|
|
1574
|
+
label: resolved.name || resolved.key,
|
|
1575
|
+
appKey: resolved.key,
|
|
1576
|
+
environment: resolveEnvironment(flags, 'production'),
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1520
1579
|
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1580
|
+
async function forensicsChat(args, flags) {
|
|
1581
|
+
requireAuth();
|
|
1582
|
+
// `--json "<question>"` parses the question as the flag's value; give it back.
|
|
1583
|
+
const words = [...args];
|
|
1584
|
+
for (const key of ['json', 'yes', 'force']) {
|
|
1585
|
+
if (typeof flags[key] === 'string') { words.push(flags[key]); flags[key] = true; }
|
|
1586
|
+
}
|
|
1587
|
+
const target = await resolveChatTarget(flags);
|
|
1588
|
+
const { environment } = target;
|
|
1589
|
+
let conversationId = target.conversationId;
|
|
1590
|
+
const question = words.join(' ').trim();
|
|
1591
|
+
|
|
1592
|
+
// One question, one reply, then exit: scriptable, and what an agent without MCP uses.
|
|
1593
|
+
if (question) {
|
|
1594
|
+
const s = flags.json ? null : ui.spinner('Thinking');
|
|
1595
|
+
const body = { message: question, environment };
|
|
1596
|
+
if (conversationId) body.conversationId = conversationId;
|
|
1597
|
+
else body.applicationKey = target.appKey;
|
|
1598
|
+
let reply = await sendChatMessage(body, environment, s);
|
|
1599
|
+
if (reply.status === 'awaiting_confirmation' && (flags.yes || flags.force)) {
|
|
1600
|
+
if (s) s.update('Running the large query');
|
|
1601
|
+
reply = await confirmChatQuery(reply.conversationId, environment, s);
|
|
1602
|
+
}
|
|
1603
|
+
if (flags.json) { ui.json(reply); return; }
|
|
1604
|
+
if (reply.status === 'processing') {
|
|
1605
|
+
s.fail('Still working');
|
|
1606
|
+
ui.info(`See the reply later: securenow forensics conversations ${reply.conversationId}`);
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
s.stop('Done');
|
|
1610
|
+
printChatMessage(reply.message);
|
|
1611
|
+
console.log('');
|
|
1612
|
+
if (reply.status === 'awaiting_confirmation') {
|
|
1613
|
+
ui.info('Large query held back. Add --yes to run it, or ask a narrower question.');
|
|
1614
|
+
}
|
|
1615
|
+
ui.info(`Conversation ${reply.conversationId}`);
|
|
1616
|
+
console.log(ui.c.dim(` Follow up: securenow forensics chat --conversation ${reply.conversationId} "<question>"`));
|
|
1617
|
+
console.log('');
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1525
1620
|
|
|
1526
|
-
|
|
1621
|
+
console.log('');
|
|
1622
|
+
ui.heading(`Forensics Chat — ${target.label}`);
|
|
1623
|
+
if (conversationId) {
|
|
1624
|
+
console.log(ui.c.dim(` Continuing: ${target.title} (${target.messageCount} messages)`));
|
|
1625
|
+
} else {
|
|
1626
|
+
console.log(ui.c.dim(` App: ${target.appKey}`));
|
|
1627
|
+
}
|
|
1527
1628
|
console.log(ui.c.dim(` Env: ${environment}`));
|
|
1528
|
-
|
|
1529
|
-
let conversationId = null;
|
|
1629
|
+
console.log(ui.c.dim(' Type your question, or "exit" to quit.\n'));
|
|
1530
1630
|
|
|
1531
1631
|
const readline = require('readline');
|
|
1532
1632
|
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
@@ -1541,57 +1641,37 @@ async function forensicsChat(args, flags) {
|
|
|
1541
1641
|
if (!message || message.toLowerCase() === 'exit' || message.toLowerCase() === 'quit') {
|
|
1542
1642
|
console.log('');
|
|
1543
1643
|
ui.info('Chat ended.');
|
|
1644
|
+
if (conversationId) console.log(ui.c.dim(` Continue later: securenow forensics chat --conversation ${conversationId}`));
|
|
1544
1645
|
break;
|
|
1545
1646
|
}
|
|
1546
1647
|
|
|
1547
1648
|
const s = ui.spinner('Thinking');
|
|
1548
1649
|
try {
|
|
1549
|
-
const body = { message,
|
|
1650
|
+
const body = { message, environment };
|
|
1550
1651
|
if (conversationId) body.conversationId = conversationId;
|
|
1652
|
+
else body.applicationKey = target.appKey;
|
|
1551
1653
|
|
|
1552
|
-
const
|
|
1553
|
-
conversationId =
|
|
1554
|
-
|
|
1555
|
-
if (chatRes.status === 'processing') {
|
|
1556
|
-
let result;
|
|
1557
|
-
for (let i = 0; i < 150; i++) {
|
|
1558
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
1559
|
-
result = await api.get(`/forensics/chat/status/${conversationId}`, { query: { environment } });
|
|
1560
|
-
if (result.status === 'complete' || result.status === 'failed' || result.status === 'awaiting_confirmation') break;
|
|
1561
|
-
const progress = result._progress;
|
|
1562
|
-
if (progress?.action) s.update(progress.action);
|
|
1563
|
-
}
|
|
1654
|
+
const reply = await sendChatMessage(body, environment, s);
|
|
1655
|
+
conversationId = reply.conversationId;
|
|
1564
1656
|
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1657
|
+
if (reply.status === 'processing') {
|
|
1658
|
+
s.fail('Response timed out');
|
|
1659
|
+
continue;
|
|
1660
|
+
}
|
|
1569
1661
|
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
if (confirmResult.status !== 'processing') break;
|
|
1583
|
-
}
|
|
1584
|
-
cs.stop('Done');
|
|
1585
|
-
if (confirmResult?.message) printChatMessage(confirmResult.message);
|
|
1586
|
-
} else {
|
|
1587
|
-
ui.info('Query skipped. Ask a different question or refine.');
|
|
1588
|
-
}
|
|
1662
|
+
s.stop('Done');
|
|
1663
|
+
printChatMessage(reply.message);
|
|
1664
|
+
|
|
1665
|
+
if (reply.status === 'awaiting_confirmation') {
|
|
1666
|
+
const proceed = await ui.confirm('Proceed with this query?');
|
|
1667
|
+
if (proceed) {
|
|
1668
|
+
const cs = ui.spinner('Executing query');
|
|
1669
|
+
const confirmed = await confirmChatQuery(conversationId, environment, cs);
|
|
1670
|
+
cs.stop('Done');
|
|
1671
|
+
if (confirmed?.message) printChatMessage(confirmed.message);
|
|
1672
|
+
} else {
|
|
1673
|
+
ui.info('Query skipped. Ask a different question or refine.');
|
|
1589
1674
|
}
|
|
1590
|
-
} else if (chatRes.message) {
|
|
1591
|
-
s.stop('Done');
|
|
1592
|
-
printChatMessage(chatRes.message);
|
|
1593
|
-
} else {
|
|
1594
|
-
s.stop('Done');
|
|
1595
1675
|
}
|
|
1596
1676
|
} catch (err) {
|
|
1597
1677
|
s.fail('Error');
|
|
@@ -1604,9 +1684,61 @@ async function forensicsChat(args, flags) {
|
|
|
1604
1684
|
}
|
|
1605
1685
|
}
|
|
1606
1686
|
|
|
1607
|
-
function
|
|
1687
|
+
async function forensicsConversations(args, flags) {
|
|
1688
|
+
requireAuth();
|
|
1689
|
+
const id = args[0];
|
|
1690
|
+
|
|
1691
|
+
if (id) {
|
|
1692
|
+
const s = flags.json ? null : ui.spinner('Fetching conversation');
|
|
1693
|
+
const data = await api.get(`/forensics/conversations/${id}`);
|
|
1694
|
+
const conv = data.conversation;
|
|
1695
|
+
if (flags.json) { ui.json(conv); return; }
|
|
1696
|
+
s.stop(conv.title);
|
|
1697
|
+
console.log(ui.c.dim(` ${conv.applicationName || conv.applicationKey} · ${conv.environment} · ${(conv.messages || []).length} messages · ${conv.id}`));
|
|
1698
|
+
for (const msg of conv.messages || []) {
|
|
1699
|
+
if (msg.role === 'user') {
|
|
1700
|
+
console.log(`\n ${ui.c.cyan('you >')} ${msg.content}`);
|
|
1701
|
+
} else {
|
|
1702
|
+
printChatMessage(msg, { maxRows: 10 });
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
console.log('');
|
|
1706
|
+
console.log(ui.c.dim(` Continue: securenow forensics chat --conversation ${conv.id} "<question>"`));
|
|
1707
|
+
console.log('');
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
const s = flags.json ? null : ui.spinner('Fetching conversations');
|
|
1712
|
+
const query = {};
|
|
1713
|
+
if (flags.app) {
|
|
1714
|
+
const resolved = await resolveAppId(flags);
|
|
1715
|
+
if (resolved) query.applicationId = resolved.id;
|
|
1716
|
+
}
|
|
1717
|
+
const data = await api.get('/forensics/conversations', { query });
|
|
1718
|
+
const list = data.conversations || [];
|
|
1719
|
+
if (flags.json) { ui.json(list); return; }
|
|
1720
|
+
s.stop(`Found ${list.length} conversation${list.length !== 1 ? 's' : ''}`);
|
|
1721
|
+
console.log('');
|
|
1722
|
+
ui.table(['ID', 'Title', 'App', 'Env', 'Msgs', 'Updated'], list.map(c => [
|
|
1723
|
+
ui.c.dim(c.id),
|
|
1724
|
+
ui.truncate(c.title || '', 48),
|
|
1725
|
+
c.applicationName || '—',
|
|
1726
|
+
c.environment || '—',
|
|
1727
|
+
String(c.messageCount ?? 0),
|
|
1728
|
+
ui.timeAgo(c.updatedAt),
|
|
1729
|
+
]));
|
|
1730
|
+
console.log('');
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
function printChatMessage(msg, { maxRows = CHAT_PRINT_ROWS } = {}) {
|
|
1608
1734
|
if (!msg) return;
|
|
1609
1735
|
console.log('');
|
|
1736
|
+
if (msg.reasoning) {
|
|
1737
|
+
console.log(ui.c.dim(` plan: ${msg.reasoning}`));
|
|
1738
|
+
}
|
|
1739
|
+
if (Array.isArray(msg.discoverySteps) && msg.discoverySteps.length > 0) {
|
|
1740
|
+
console.log(ui.c.dim(` ${msg.discoverySteps.length} discovery quer${msg.discoverySteps.length !== 1 ? 'ies' : 'y'} run first`));
|
|
1741
|
+
}
|
|
1610
1742
|
if (msg.content) {
|
|
1611
1743
|
console.log(` ${ui.c.green('ai >')} ${msg.content}`);
|
|
1612
1744
|
}
|
|
@@ -1616,11 +1748,18 @@ function printChatMessage(msg) {
|
|
|
1616
1748
|
console.log(`\n ${ui.c.dim(msg.sqlQuery)}\n`);
|
|
1617
1749
|
}
|
|
1618
1750
|
if (msg.results && Array.isArray(msg.results) && msg.results.length > 0) {
|
|
1619
|
-
|
|
1751
|
+
const total = msg.resultCount ?? msg.results.length;
|
|
1752
|
+
ui.subheading(`Results (${total} rows)`);
|
|
1620
1753
|
console.log('');
|
|
1621
1754
|
const headers = Object.keys(msg.results[0]);
|
|
1622
|
-
const rows = msg.results.map(row => headers.map(h =>
|
|
1755
|
+
const rows = msg.results.slice(0, maxRows).map(row => headers.map(h => chatCell(row[h])));
|
|
1623
1756
|
ui.table(headers, rows);
|
|
1757
|
+
if (msg.results.length > maxRows) {
|
|
1758
|
+
console.log(ui.c.dim(` … ${msg.results.length - maxRows} more rows (--json for all)`));
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
if (msg.findings) {
|
|
1762
|
+
console.log(`\n ${ui.c.cyan('findings >')} ${msg.findings}`);
|
|
1624
1763
|
}
|
|
1625
1764
|
if (msg.estimation) {
|
|
1626
1765
|
const est = msg.estimation;
|
|
@@ -1857,6 +1996,7 @@ module.exports = {
|
|
|
1857
1996
|
trustedRemove,
|
|
1858
1997
|
forensicsQuery,
|
|
1859
1998
|
forensicsChat,
|
|
1999
|
+
forensicsConversations,
|
|
1860
2000
|
forensicsLibrary,
|
|
1861
2001
|
ipLookup,
|
|
1862
2002
|
ipTraces,
|
package/cli.js
CHANGED
|
@@ -585,7 +585,8 @@ const COMMANDS = {
|
|
|
585
585
|
usage: 'securenow forensics <query> [--app <key>]',
|
|
586
586
|
sub: {
|
|
587
587
|
query: { desc: 'Run a forensic query', flags: { app: 'App key to scope query', env: 'Environment (default: production)', environment: 'Alias for --env' }, run: (a, f) => require('./cli/security').forensicsQuery(a, f) },
|
|
588
|
-
chat: { desc: '
|
|
588
|
+
chat: { desc: 'Forensics chat with memory: interactive, or one question with "<question>"', usage: 'securenow forensics chat [--app <key> | --conversation <id>] ["<question>"] [--json]', flags: { app: 'App key for a new conversation', conversation: 'Continue this conversation (from the dashboard, the CLI or an agent)', env: 'Environment of a new conversation (default: production)', environment: 'Alias for --env', yes: 'Run a query the chat held back as large', json: 'Print the raw reply (one-question mode)' }, run: (a, f) => require('./cli/security').forensicsChat(a, f) },
|
|
589
|
+
conversations: { desc: 'List forensics chat conversations, or show one', usage: 'securenow forensics conversations [<id>] [--app <key>] [--json]', flags: { app: 'Only conversations about this app', json: 'Output as JSON' }, run: (a, f) => require('./cli/security').forensicsConversations(a, f) },
|
|
589
590
|
library: { desc: 'View saved queries', run: (a, f) => require('./cli/security').forensicsLibrary(a, f) },
|
|
590
591
|
},
|
|
591
592
|
defaultAction: (a, f) => require('./cli/security').forensicsQuery(a, f),
|
package/mcp/catalog.js
CHANGED
|
@@ -845,7 +845,7 @@ const TOOLS = [
|
|
|
845
845
|
{
|
|
846
846
|
name: 'securenow_forensics_query',
|
|
847
847
|
title: 'Run Forensics Query',
|
|
848
|
-
description: 'Run
|
|
848
|
+
description: 'Run one natural-language forensic query over your traffic and return the result rows. One question, no memory of earlier questions; for an investigation with follow-ups use securenow_forensics_chat. Waits up to 30 seconds; a query still running after that returns its jobId with status processing. Write scope because it creates an async query job.',
|
|
849
849
|
scope: 'forensics:write',
|
|
850
850
|
readOnly: false,
|
|
851
851
|
confirm: true,
|
|
@@ -890,6 +890,93 @@ const TOOLS = [
|
|
|
890
890
|
endpoint: '/forensics/query-library',
|
|
891
891
|
inputSchema: objectSchema({}),
|
|
892
892
|
},
|
|
893
|
+
{
|
|
894
|
+
name: 'securenow_forensics_chat',
|
|
895
|
+
title: 'Ask the Forensics Chat',
|
|
896
|
+
description: 'Ask the SecureNow forensics chat a question about one app\'s traces, logs, errors and custom events, in plain English. Without conversationId it starts a new conversation (applicationKey or applicationId required); with conversationId it continues that conversation, including ones started in the dashboard or the CLI, and the chat sees the earlier questions, their SQL, findings, and the IPs and trace IDs in their results. The reply has the chat\'s reasoning, any discovery queries, the SQL, up to 100 result rows and findings: what the rows show. Read-only over telemetry. Waits up to 90 seconds; a reply still running after that comes back with status processing (use securenow_forensics_chat_status). Status awaiting_confirmation means the query is large: run it with securenow_forensics_chat_confirm or ask a narrower question.',
|
|
897
|
+
scope: 'forensics:write',
|
|
898
|
+
readOnly: false,
|
|
899
|
+
confirm: true,
|
|
900
|
+
method: 'POST',
|
|
901
|
+
endpoint: '/forensics/chat',
|
|
902
|
+
bodyFields: ['message', 'conversationId', 'applicationKey', 'applicationId', 'environment'],
|
|
903
|
+
poll: {
|
|
904
|
+
tool: 'securenow_forensics_chat_status',
|
|
905
|
+
idField: 'conversationId',
|
|
906
|
+
pending: ['processing'],
|
|
907
|
+
timeoutMs: 90000,
|
|
908
|
+
intervalMs: 2000,
|
|
909
|
+
},
|
|
910
|
+
inputSchema: objectSchema({
|
|
911
|
+
message: string('The question, in plain English. Follow-ups can refer to earlier answers ("those IPs", "that session").'),
|
|
912
|
+
conversationId: string('Continue this conversation. Omit to start a new one.'),
|
|
913
|
+
applicationKey: string('App key the new conversation is scoped to. Not needed with conversationId.'),
|
|
914
|
+
applicationId: string('App database id, instead of applicationKey.'),
|
|
915
|
+
environment: string('Environment of a new conversation: production (default), staging, preview, local or test. Omit it when continuing: a conversation keeps its environment.'),
|
|
916
|
+
...confirmSchema,
|
|
917
|
+
}, ['message', 'confirm', 'reason']),
|
|
918
|
+
},
|
|
919
|
+
{
|
|
920
|
+
name: 'securenow_forensics_chat_status',
|
|
921
|
+
title: 'Forensics Chat Reply',
|
|
922
|
+
description: 'Get the latest reply of a forensics chat conversation: status (processing, complete or awaiting_confirmation), progress while processing, then the reasoning, SQL, result rows and findings.',
|
|
923
|
+
scope: 'forensics:read',
|
|
924
|
+
readOnly: true,
|
|
925
|
+
method: 'GET',
|
|
926
|
+
endpoint: '/forensics/chat/status/:conversationId',
|
|
927
|
+
pathParams: ['conversationId'],
|
|
928
|
+
inputSchema: objectSchema({
|
|
929
|
+
conversationId: string('Forensics chat conversation id.'),
|
|
930
|
+
}, ['conversationId']),
|
|
931
|
+
},
|
|
932
|
+
{
|
|
933
|
+
name: 'securenow_forensics_chat_confirm',
|
|
934
|
+
title: 'Run a Large Forensics Query',
|
|
935
|
+
description: 'Run the query a forensics chat held back as large (status awaiting_confirmation), after the user agrees. Returns the rows and findings like securenow_forensics_chat.',
|
|
936
|
+
scope: 'forensics:write',
|
|
937
|
+
readOnly: false,
|
|
938
|
+
confirm: true,
|
|
939
|
+
method: 'POST',
|
|
940
|
+
endpoint: '/forensics/chat/confirm/:conversationId',
|
|
941
|
+
pathParams: ['conversationId'],
|
|
942
|
+
poll: {
|
|
943
|
+
tool: 'securenow_forensics_chat_status',
|
|
944
|
+
idField: 'conversationId',
|
|
945
|
+
pending: ['processing'],
|
|
946
|
+
timeoutMs: 90000,
|
|
947
|
+
intervalMs: 2000,
|
|
948
|
+
},
|
|
949
|
+
inputSchema: objectSchema({
|
|
950
|
+
conversationId: string('Forensics chat conversation id.'),
|
|
951
|
+
...confirmSchema,
|
|
952
|
+
}, ['conversationId', 'confirm', 'reason']),
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
name: 'securenow_forensics_conversations_list',
|
|
956
|
+
title: 'List Forensics Conversations',
|
|
957
|
+
description: 'List your forensics chat conversations, newest first (up to 100): id, title, app, environment, message count and last update. Includes conversations started in the dashboard and the CLI.',
|
|
958
|
+
scope: 'forensics:read',
|
|
959
|
+
readOnly: true,
|
|
960
|
+
method: 'GET',
|
|
961
|
+
endpoint: '/forensics/conversations',
|
|
962
|
+
queryFields: ['applicationId'],
|
|
963
|
+
inputSchema: objectSchema({
|
|
964
|
+
applicationId: string('Only conversations about this app database id.'),
|
|
965
|
+
}),
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
name: 'securenow_forensics_conversation_get',
|
|
969
|
+
title: 'Get Forensics Conversation',
|
|
970
|
+
description: 'Get a whole forensics chat conversation: every question and reply with its reasoning, SQL, findings and up to 100 result rows. Use it to read an investigation someone started in the dashboard before continuing it with securenow_forensics_chat.',
|
|
971
|
+
scope: 'forensics:read',
|
|
972
|
+
readOnly: true,
|
|
973
|
+
method: 'GET',
|
|
974
|
+
endpoint: '/forensics/conversations/:id',
|
|
975
|
+
pathParams: ['id'],
|
|
976
|
+
inputSchema: objectSchema({
|
|
977
|
+
id: string('Forensics chat conversation id.'),
|
|
978
|
+
}, ['id']),
|
|
979
|
+
},
|
|
893
980
|
{
|
|
894
981
|
name: 'securenow_analytics_summary',
|
|
895
982
|
title: 'Analytics Summary',
|
package/package.json
CHANGED