@exfil/canary 1.0.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/LICENSE +21 -0
- package/README.md +387 -0
- package/SECURITY.md +50 -0
- package/dist/entities.d.ts +43 -0
- package/dist/entities.d.ts.map +1 -0
- package/dist/entities.js +218 -0
- package/dist/entities.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +183 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +29 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +50 -0
- package/dist/logger.js.map +1 -0
- package/dist/persistence.d.ts +48 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +296 -0
- package/dist/persistence.js.map +1 -0
- package/dist/proxy/DownstreamManager.d.ts +55 -0
- package/dist/proxy/DownstreamManager.d.ts.map +1 -0
- package/dist/proxy/DownstreamManager.js +110 -0
- package/dist/proxy/DownstreamManager.js.map +1 -0
- package/dist/proxy/ProxyServer.d.ts +60 -0
- package/dist/proxy/ProxyServer.d.ts.map +1 -0
- package/dist/proxy/ProxyServer.js +480 -0
- package/dist/proxy/ProxyServer.js.map +1 -0
- package/dist/proxy/auditor/DualAuditor.d.ts +27 -0
- package/dist/proxy/auditor/DualAuditor.d.ts.map +1 -0
- package/dist/proxy/auditor/DualAuditor.js +44 -0
- package/dist/proxy/auditor/DualAuditor.js.map +1 -0
- package/dist/proxy/auditor/LLMAuditor.d.ts +16 -0
- package/dist/proxy/auditor/LLMAuditor.d.ts.map +1 -0
- package/dist/proxy/auditor/LLMAuditor.js +221 -0
- package/dist/proxy/auditor/LLMAuditor.js.map +1 -0
- package/dist/proxy/auditor/types.d.ts +54 -0
- package/dist/proxy/auditor/types.d.ts.map +1 -0
- package/dist/proxy/auditor/types.js +11 -0
- package/dist/proxy/auditor/types.js.map +1 -0
- package/dist/proxy/types.d.ts +71 -0
- package/dist/proxy/types.d.ts.map +1 -0
- package/dist/proxy/types.js +8 -0
- package/dist/proxy/types.js.map +1 -0
- package/dist/scanner.d.ts +37 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +57 -0
- package/dist/scanner.js.map +1 -0
- package/dist/server.d.ts +59 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +711 -0
- package/dist/server.js.map +1 -0
- package/dist/simhash.d.ts +65 -0
- package/dist/simhash.d.ts.map +1 -0
- package/dist/simhash.js +151 -0
- package/dist/simhash.js.map +1 -0
- package/dist/state.d.ts +86 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +136 -0
- package/dist/state.js.map +1 -0
- package/dist/token.d.ts +70 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +146 -0
- package/dist/token.js.map +1 -0
- package/dist/types.d.ts +190 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/package.json +52 -0
- package/proxy.example.json +53 -0
package/dist/entities.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v1.1 — Named entity extraction for structural canary injection.
|
|
3
|
+
*
|
|
4
|
+
* Extracts identifiable values from content at wrap time and stores them as
|
|
5
|
+
* EntityCanary records alongside the Unicode sequence marker. When scanning
|
|
6
|
+
* outbound data, exfil/canary checks for the presence of these extracted values
|
|
7
|
+
* in addition to the Unicode marker — catching exfiltration that involves
|
|
8
|
+
* paraphrasing or rewriting, as long as the underlying identifier (e.g. an
|
|
9
|
+
* API key, email address, or UUID) is reproduced verbatim.
|
|
10
|
+
*
|
|
11
|
+
* SECURITY: Extracted values are treated with the same sensitivity as the
|
|
12
|
+
* Unicode sequence — they are never returned in tool outputs, never logged,
|
|
13
|
+
* and never persisted in plaintext.
|
|
14
|
+
*
|
|
15
|
+
* Detection coverage added by v1.1 vs v1.0:
|
|
16
|
+
* - Agent reads "API key: sk-abc123" and writes "key=sk-abc123" → DETECTED
|
|
17
|
+
* - Agent reads email content and includes recipient addr in forward → DETECTED
|
|
18
|
+
* - Raw copy-paste / direct forwarding → DETECTED (v1.0)
|
|
19
|
+
* - Agent summarises with different phrasing, no literal value → NOT detected (v2.0)
|
|
20
|
+
*/
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Shannon entropy (used to filter high-entropy strings)
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
/**
|
|
25
|
+
* Computes the Shannon entropy of a string in bits per character.
|
|
26
|
+
* A random 32-char hex string has entropy ≈ 4.0. English prose ≈ 2.5–3.0.
|
|
27
|
+
*/
|
|
28
|
+
function shannonEntropy(s) {
|
|
29
|
+
const freq = new Map();
|
|
30
|
+
for (const ch of s) {
|
|
31
|
+
freq.set(ch, (freq.get(ch) ?? 0) + 1);
|
|
32
|
+
}
|
|
33
|
+
let entropy = 0;
|
|
34
|
+
for (const count of freq.values()) {
|
|
35
|
+
const p = count / s.length;
|
|
36
|
+
entropy -= p * Math.log2(p);
|
|
37
|
+
}
|
|
38
|
+
return entropy;
|
|
39
|
+
}
|
|
40
|
+
/** Returns true if the string looks like a secret (entropy ≥ 3.5 bits/char). */
|
|
41
|
+
function isHighEntropy(s) {
|
|
42
|
+
return shannonEntropy(s) >= 3.5;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Patterns ordered from most-specific (lowest false positive rate) to
|
|
46
|
+
* least-specific. Processing stops adding an entity once it has been seen.
|
|
47
|
+
*/
|
|
48
|
+
const PATTERNS = [
|
|
49
|
+
// ── API keys — well-known vendor formats ────────────────────────────────
|
|
50
|
+
// OpenAI
|
|
51
|
+
{ type: 'api_key', pattern: /sk-proj-[A-Za-z0-9_-]{40,}/g },
|
|
52
|
+
{ type: 'api_key', pattern: /sk-[A-Za-z0-9]{20,}/g },
|
|
53
|
+
// Anthropic
|
|
54
|
+
{ type: 'api_key', pattern: /sk-ant-[A-Za-z0-9_-]{30,}/g },
|
|
55
|
+
// GitHub
|
|
56
|
+
{ type: 'api_key', pattern: /ghp_[A-Za-z0-9]{36}/g },
|
|
57
|
+
{ type: 'api_key', pattern: /github_pat_[A-Za-z0-9_]{82}/g },
|
|
58
|
+
// GitLab
|
|
59
|
+
{ type: 'api_key', pattern: /glpat-[A-Za-z0-9_-]{20,}/g },
|
|
60
|
+
// Slack
|
|
61
|
+
{ type: 'api_key', pattern: /xox[baprs]-[0-9A-Za-z-]{10,}/g },
|
|
62
|
+
// AWS access key ID
|
|
63
|
+
{ type: 'api_key', pattern: /(?:AKIA|ASIA)[0-9A-Z]{16}/g },
|
|
64
|
+
// Stripe
|
|
65
|
+
{ type: 'api_key', pattern: /(?:sk|pk)_(?:live|test)_[A-Za-z0-9]{24,}/g },
|
|
66
|
+
// npm
|
|
67
|
+
{ type: 'api_key', pattern: /npm_[A-Za-z0-9]{36}/g },
|
|
68
|
+
// Twilio
|
|
69
|
+
{ type: 'api_key', pattern: /SK[0-9a-f]{32}/g },
|
|
70
|
+
// SendGrid
|
|
71
|
+
{ type: 'api_key', pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/g },
|
|
72
|
+
// ── Bearer tokens ────────────────────────────────────────────────────────
|
|
73
|
+
{
|
|
74
|
+
type: 'bearer_token',
|
|
75
|
+
pattern: /Bearer\s+([-A-Za-z0-9._~+/=]{20,})/g,
|
|
76
|
+
useGroup: true,
|
|
77
|
+
validate: isHighEntropy,
|
|
78
|
+
},
|
|
79
|
+
// ── Credential assignment patterns ───────────────────────────────────────
|
|
80
|
+
// Matches: password=X, api_key=X, secret=X, token=X, auth=X, key=X
|
|
81
|
+
// Also JSON: "password": "X"
|
|
82
|
+
{
|
|
83
|
+
type: 'credential_pair',
|
|
84
|
+
pattern: /(?:password|passwd|api[_-]?key|secret|token|auth(?:_token)?|access[_-]?key)\s*[=:]\s*["']?([A-Za-z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>/?`~]{8,})["']?/gi,
|
|
85
|
+
useGroup: true,
|
|
86
|
+
validate: isHighEntropy,
|
|
87
|
+
},
|
|
88
|
+
// ── Email addresses ──────────────────────────────────────────────────────
|
|
89
|
+
{
|
|
90
|
+
type: 'email',
|
|
91
|
+
pattern: /[A-Za-z0-9._%+-]+@[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?\.[A-Za-z]{2,}/g,
|
|
92
|
+
minLength: 6,
|
|
93
|
+
},
|
|
94
|
+
// ── URLs ─────────────────────────────────────────────────────────────────
|
|
95
|
+
// Only capture URLs ≥ 20 chars to avoid short/common ones like http://x.com
|
|
96
|
+
{
|
|
97
|
+
type: 'url',
|
|
98
|
+
pattern: /https?:\/\/[^\s"'<>\]){,}]{20,}/g,
|
|
99
|
+
minLength: 20,
|
|
100
|
+
},
|
|
101
|
+
// ── IP addresses ─────────────────────────────────────────────────────────
|
|
102
|
+
{
|
|
103
|
+
type: 'ip_address',
|
|
104
|
+
// Strict: each octet 0-255
|
|
105
|
+
pattern: /\b(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)){3}\b/g,
|
|
106
|
+
minLength: 7,
|
|
107
|
+
// Ignore loopback and link-local — not sensitive
|
|
108
|
+
validate: (v) => !v.startsWith('127.') &&
|
|
109
|
+
!v.startsWith('169.254.') &&
|
|
110
|
+
v !== '0.0.0.0' &&
|
|
111
|
+
v !== '255.255.255.255',
|
|
112
|
+
},
|
|
113
|
+
// ── UUIDs ────────────────────────────────────────────────────────────────
|
|
114
|
+
{
|
|
115
|
+
type: 'uuid',
|
|
116
|
+
pattern: /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,
|
|
117
|
+
minLength: 36,
|
|
118
|
+
},
|
|
119
|
+
// ── High-entropy strings ─────────────────────────────────────────────────
|
|
120
|
+
// Long hex runs (hashes, raw keys)
|
|
121
|
+
{
|
|
122
|
+
type: 'high_entropy_string',
|
|
123
|
+
pattern: /\b[0-9a-f]{32,}\b/g,
|
|
124
|
+
validate: isHighEntropy,
|
|
125
|
+
},
|
|
126
|
+
// Base64-like strings (JWT segments, encoded tokens)
|
|
127
|
+
{
|
|
128
|
+
type: 'high_entropy_string',
|
|
129
|
+
pattern: /[A-Za-z0-9+/]{40,}={0,2}/g,
|
|
130
|
+
validate: isHighEntropy,
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Context window
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
/** Characters of surrounding text to keep in the context hint. */
|
|
137
|
+
const CONTEXT_WINDOW = 24;
|
|
138
|
+
function buildContextHint(content, matchStart, value) {
|
|
139
|
+
const start = Math.max(0, matchStart - CONTEXT_WINDOW);
|
|
140
|
+
const end = Math.min(content.length, matchStart + value.length + CONTEXT_WINDOW);
|
|
141
|
+
const snippet = content.slice(start, end);
|
|
142
|
+
// Use index-based replacement so we always redact the correct occurrence,
|
|
143
|
+
// even if the same value appears elsewhere in the snippet.
|
|
144
|
+
const valueOffset = matchStart - start;
|
|
145
|
+
return (snippet.slice(0, valueOffset) +
|
|
146
|
+
'[ENTITY]' +
|
|
147
|
+
snippet.slice(valueOffset + value.length));
|
|
148
|
+
}
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Public API
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
/**
|
|
153
|
+
* Extracts named entities from `content` for use as structural canary markers.
|
|
154
|
+
*
|
|
155
|
+
* Each unique entity value is returned at most once, even if it appears
|
|
156
|
+
* multiple times in the content. Values are de-duplicated case-sensitively
|
|
157
|
+
* for exact matching.
|
|
158
|
+
*
|
|
159
|
+
* @param content The raw content to analyse (before embedding the Unicode token).
|
|
160
|
+
* @returns Array of EntityCanary records. May be empty if no entities found.
|
|
161
|
+
*/
|
|
162
|
+
export function extractEntities(content) {
|
|
163
|
+
const results = [];
|
|
164
|
+
const seen = new Set();
|
|
165
|
+
for (const desc of PATTERNS) {
|
|
166
|
+
const { type, pattern, useGroup, validate, minLength = 8 } = desc;
|
|
167
|
+
// Reset the global regex's lastIndex before each use.
|
|
168
|
+
pattern.lastIndex = 0;
|
|
169
|
+
let match;
|
|
170
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
171
|
+
// For patterns that capture the secret in group 1, prefer group 1.
|
|
172
|
+
const raw = useGroup ? (match[1] ?? match[0]) : match[0];
|
|
173
|
+
// Trim surrounding whitespace/quotes that may have bled in.
|
|
174
|
+
const value = raw.trimEnd().replace(/^["'\s]+|["'\s]+$/g, '');
|
|
175
|
+
if (value.length < minLength)
|
|
176
|
+
continue;
|
|
177
|
+
if (validate && !validate(value))
|
|
178
|
+
continue;
|
|
179
|
+
if (seen.has(value))
|
|
180
|
+
continue;
|
|
181
|
+
seen.add(value);
|
|
182
|
+
// Compute the match start position in the original string.
|
|
183
|
+
// When useGroup, the capture group starts after any prefix in group 0.
|
|
184
|
+
const groupOffset = useGroup
|
|
185
|
+
? match[0].indexOf(raw)
|
|
186
|
+
: 0;
|
|
187
|
+
const matchStart = match.index + groupOffset;
|
|
188
|
+
results.push({
|
|
189
|
+
entity_type: type,
|
|
190
|
+
value,
|
|
191
|
+
context_hint: buildContextHint(content, matchStart, value),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return results;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Checks whether any of the provided entity values appear in `data`.
|
|
199
|
+
* Returns the matching entity canaries (values excluded from returned objects
|
|
200
|
+
* — callers receive entity_type + context_hint only when building reports).
|
|
201
|
+
*
|
|
202
|
+
* @param data The outbound string to scan.
|
|
203
|
+
* @param canaries Entity canaries to check for.
|
|
204
|
+
* @returns Array of matched canaries (values intact for internal use only).
|
|
205
|
+
*/
|
|
206
|
+
export function scanForEntityValues(data, canaries) {
|
|
207
|
+
const matches = [];
|
|
208
|
+
for (const canary of canaries) {
|
|
209
|
+
// Empty value = recovered from persistence, cannot re-detect.
|
|
210
|
+
if (!canary.value)
|
|
211
|
+
continue;
|
|
212
|
+
if (data.includes(canary.value)) {
|
|
213
|
+
matches.push(canary);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return matches;
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=entities.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entities.js","sourceRoot":"","sources":["../src/entities.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* exfil/canary entry point.
|
|
4
|
+
*
|
|
5
|
+
* Responsibilities:
|
|
6
|
+
* 1. Read and validate all environment variables (fast-fail on bad config).
|
|
7
|
+
* 2. Attempt state recovery from persistence file.
|
|
8
|
+
* 3. Construct and start the CanaryMcpServer.
|
|
9
|
+
* 4. Register SIGTERM / SIGINT handlers for graceful shutdown.
|
|
10
|
+
* 5. Start a 60-second expiry sweep interval (unref'd so it does not keep
|
|
11
|
+
* the Node process alive past the server lifecycle).
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;GAUG"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* exfil/canary entry point.
|
|
4
|
+
*
|
|
5
|
+
* Responsibilities:
|
|
6
|
+
* 1. Read and validate all environment variables (fast-fail on bad config).
|
|
7
|
+
* 2. Attempt state recovery from persistence file.
|
|
8
|
+
* 3. Construct and start the CanaryMcpServer.
|
|
9
|
+
* 4. Register SIGTERM / SIGINT handlers for graceful shutdown.
|
|
10
|
+
* 5. Start a 60-second expiry sweep interval (unref'd so it does not keep
|
|
11
|
+
* the Node process alive past the server lifecycle).
|
|
12
|
+
*/
|
|
13
|
+
import { readFileSync } from 'fs';
|
|
14
|
+
import { validateWebhookUrl } from './server.js';
|
|
15
|
+
import { CanaryMcpServer } from './server.js';
|
|
16
|
+
import { recoverState } from './persistence.js';
|
|
17
|
+
import { createSession } from './state.js';
|
|
18
|
+
import { setLogLevel, log } from './logger.js';
|
|
19
|
+
import { ProxyServer } from './proxy/ProxyServer.js';
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Environment variable parsing
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
/**
|
|
24
|
+
* Parses and validates all environment variables.
|
|
25
|
+
* Throws a descriptive error on any misconfiguration (fast-fail, RC-2).
|
|
26
|
+
*
|
|
27
|
+
* @returns Validated `CanaryConfig`.
|
|
28
|
+
*/
|
|
29
|
+
function loadConfig() {
|
|
30
|
+
// Log level — set early so subsequent validation errors are visible.
|
|
31
|
+
const rawLogLevel = process.env['CANARY_MCP_LOG_LEVEL'] ?? 'info';
|
|
32
|
+
const validLogLevels = ['debug', 'info', 'warn', 'error'];
|
|
33
|
+
if (!validLogLevels.includes(rawLogLevel)) {
|
|
34
|
+
throw new Error(`CANARY_MCP_LOG_LEVEL must be one of ${validLogLevels.join(', ')} — got "${rawLogLevel}"`);
|
|
35
|
+
}
|
|
36
|
+
const log_level = rawLogLevel;
|
|
37
|
+
setLogLevel(log_level);
|
|
38
|
+
// Token TTL.
|
|
39
|
+
const rawTtl = process.env['CANARY_MCP_TOKEN_TTL'] ?? '3600';
|
|
40
|
+
const token_ttl_seconds = parseInt(rawTtl, 10);
|
|
41
|
+
if (!Number.isInteger(token_ttl_seconds) || token_ttl_seconds < 60 || token_ttl_seconds > 86400) {
|
|
42
|
+
throw new Error(`CANARY_MCP_TOKEN_TTL must be an integer between 60 and 86400 — got "${rawTtl}"`);
|
|
43
|
+
}
|
|
44
|
+
// Response mode.
|
|
45
|
+
const rawMode = process.env['CANARY_MCP_RESPONSE_MODE'] ?? 'log';
|
|
46
|
+
const validModes = ['log', 'halt', 'alert'];
|
|
47
|
+
if (!validModes.includes(rawMode)) {
|
|
48
|
+
throw new Error(`CANARY_MCP_RESPONSE_MODE must be one of ${validModes.join(', ')} — got "${rawMode}"`);
|
|
49
|
+
}
|
|
50
|
+
const response_mode = rawMode;
|
|
51
|
+
// Webhook URL (RC-6: HTTPS-only + SSRF check at startup).
|
|
52
|
+
const rawWebhook = process.env['CANARY_MCP_ALERT_WEBHOOK'];
|
|
53
|
+
let alert_webhook = null;
|
|
54
|
+
if (rawWebhook) {
|
|
55
|
+
alert_webhook = validateWebhookUrl(rawWebhook); // throws on invalid
|
|
56
|
+
}
|
|
57
|
+
if (response_mode === 'alert' && !alert_webhook) {
|
|
58
|
+
throw new Error('CANARY_MCP_RESPONSE_MODE is "alert" but CANARY_MCP_ALERT_WEBHOOK is not set.');
|
|
59
|
+
}
|
|
60
|
+
// Webhook secret (optional).
|
|
61
|
+
const webhook_secret = process.env['CANARY_MCP_WEBHOOK_SECRET'] ?? null;
|
|
62
|
+
// Persistence path (optional).
|
|
63
|
+
const persist_path = process.env['CANARY_MCP_PERSIST_PATH'] ?? null;
|
|
64
|
+
return {
|
|
65
|
+
persist_path,
|
|
66
|
+
token_ttl_seconds,
|
|
67
|
+
alert_webhook,
|
|
68
|
+
webhook_secret,
|
|
69
|
+
response_mode,
|
|
70
|
+
log_level,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Loads and parses the proxy config JSON file at the given path.
|
|
75
|
+
* Throws a descriptive error on parse failure or missing required fields.
|
|
76
|
+
*/
|
|
77
|
+
function loadProxyConfig(filePath) {
|
|
78
|
+
let raw;
|
|
79
|
+
try {
|
|
80
|
+
raw = readFileSync(filePath, 'utf-8');
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
throw new Error(`Cannot read CANARY_MCP_PROXY_CONFIG file "${filePath}": ${err.message}`);
|
|
84
|
+
}
|
|
85
|
+
let parsed;
|
|
86
|
+
try {
|
|
87
|
+
parsed = JSON.parse(raw);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
throw new Error(`CANARY_MCP_PROXY_CONFIG file is not valid JSON: ${err.message}`);
|
|
91
|
+
}
|
|
92
|
+
if (typeof parsed !== 'object' ||
|
|
93
|
+
parsed === null ||
|
|
94
|
+
!Array.isArray(parsed.servers)) {
|
|
95
|
+
throw new Error('CANARY_MCP_PROXY_CONFIG must be a JSON object with a "servers" array.');
|
|
96
|
+
}
|
|
97
|
+
return parsed;
|
|
98
|
+
}
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Main
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
async function main() {
|
|
103
|
+
let config;
|
|
104
|
+
try {
|
|
105
|
+
config = loadConfig();
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
// Log to stderr before the logger is fully initialised.
|
|
109
|
+
process.stderr.write(JSON.stringify({ level: 'error', msg: `Startup configuration error: ${err.message}` }) + '\n');
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
log('info', 'exfil/canary initialising.', {
|
|
113
|
+
response_mode: config.response_mode,
|
|
114
|
+
token_ttl_seconds: config.token_ttl_seconds,
|
|
115
|
+
persist: config.persist_path !== null,
|
|
116
|
+
webhook: config.alert_webhook !== null,
|
|
117
|
+
});
|
|
118
|
+
// Attempt state recovery; fall back to fresh session.
|
|
119
|
+
let state = await recoverState(config);
|
|
120
|
+
if (!state) {
|
|
121
|
+
state = createSession();
|
|
122
|
+
log('info', 'Starting fresh session.', { session_id: state.session_id });
|
|
123
|
+
}
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Choose mode: proxy (CANARY_MCP_PROXY_CONFIG set) or standalone
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
const proxyConfigPath = process.env['CANARY_MCP_PROXY_CONFIG'] ?? null;
|
|
128
|
+
let server;
|
|
129
|
+
if (proxyConfigPath) {
|
|
130
|
+
let proxyConfig;
|
|
131
|
+
try {
|
|
132
|
+
proxyConfig = loadProxyConfig(proxyConfigPath);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
process.stderr.write(JSON.stringify({ level: 'error', msg: `Proxy config error: ${err.message}` }) + '\n');
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
log('info', 'exfil/canary starting in proxy mode.', {
|
|
139
|
+
proxy_config: proxyConfigPath,
|
|
140
|
+
downstream_servers: proxyConfig.servers.length,
|
|
141
|
+
});
|
|
142
|
+
server = new ProxyServer(state, config, proxyConfig);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
log('info', 'exfil/canary starting in standalone mode.');
|
|
146
|
+
server = new CanaryMcpServer(state, config);
|
|
147
|
+
}
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Graceful shutdown handlers
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
let shuttingDown = false;
|
|
152
|
+
async function shutdown(signal) {
|
|
153
|
+
if (shuttingDown)
|
|
154
|
+
return;
|
|
155
|
+
shuttingDown = true;
|
|
156
|
+
log('info', `Received ${signal} — shutting down.`);
|
|
157
|
+
try {
|
|
158
|
+
await server.shutdown();
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
log('error', `Error during shutdown: ${err.message}`);
|
|
162
|
+
}
|
|
163
|
+
process.exit(0);
|
|
164
|
+
}
|
|
165
|
+
process.on('SIGTERM', () => { shutdown('SIGTERM').catch(() => process.exit(1)); });
|
|
166
|
+
process.on('SIGINT', () => { shutdown('SIGINT').catch(() => process.exit(1)); });
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Background expiry sweep — every 60s, unref'd (RC-2 / spec requirement)
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
const sweepInterval = setInterval(() => {
|
|
171
|
+
server.sweepExpiredTokens();
|
|
172
|
+
}, 60_000);
|
|
173
|
+
sweepInterval.unref();
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Start server
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
await server.start();
|
|
178
|
+
}
|
|
179
|
+
main().catch((err) => {
|
|
180
|
+
process.stderr.write(JSON.stringify({ level: 'error', msg: `Fatal: ${err.message}` }) + '\n');
|
|
181
|
+
process.exit(1);
|
|
182
|
+
});
|
|
183
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal structured logger.
|
|
3
|
+
*
|
|
4
|
+
* SECURITY: This module must NEVER log the `sequence` field of a CanaryToken,
|
|
5
|
+
* nor the raw `data` argument of scan_outbound. Callers are responsible for
|
|
6
|
+
* not passing sensitive values; this module provides no automatic redaction.
|
|
7
|
+
*
|
|
8
|
+
* Output goes to stderr so it does not interfere with the MCP stdio transport
|
|
9
|
+
* (which uses stdout).
|
|
10
|
+
*/
|
|
11
|
+
import type { LogLevel } from './types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Sets the minimum log level. Messages below this level are suppressed.
|
|
14
|
+
*
|
|
15
|
+
* @param level New minimum level.
|
|
16
|
+
*/
|
|
17
|
+
export declare function setLogLevel(level: LogLevel): void;
|
|
18
|
+
/**
|
|
19
|
+
* Emits a log line to stderr.
|
|
20
|
+
*
|
|
21
|
+
* Fields are serialised as JSON; the message string is NOT interpolated to
|
|
22
|
+
* avoid accidental inclusion of raw data.
|
|
23
|
+
*
|
|
24
|
+
* @param level Severity.
|
|
25
|
+
* @param message Human-readable description. MUST NOT contain token sequences or raw scan data.
|
|
26
|
+
* @param meta Optional additional structured fields (MUST be pre-redacted by caller).
|
|
27
|
+
*/
|
|
28
|
+
export declare function log(level: LogLevel, message: string, meta?: Record<string, unknown>): void;
|
|
29
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAY3C;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAEjD;AAED;;;;;;;;;GASG;AACH,wBAAgB,GAAG,CACjB,KAAK,EAAE,QAAQ,EACf,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,IAAI,CAaN"}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal structured logger.
|
|
3
|
+
*
|
|
4
|
+
* SECURITY: This module must NEVER log the `sequence` field of a CanaryToken,
|
|
5
|
+
* nor the raw `data` argument of scan_outbound. Callers are responsible for
|
|
6
|
+
* not passing sensitive values; this module provides no automatic redaction.
|
|
7
|
+
*
|
|
8
|
+
* Output goes to stderr so it does not interfere with the MCP stdio transport
|
|
9
|
+
* (which uses stdout).
|
|
10
|
+
*/
|
|
11
|
+
const LEVEL_RANK = {
|
|
12
|
+
debug: 0,
|
|
13
|
+
info: 1,
|
|
14
|
+
warn: 2,
|
|
15
|
+
error: 3,
|
|
16
|
+
};
|
|
17
|
+
/** The minimum level to emit. Set once at startup via `setLogLevel`. */
|
|
18
|
+
let currentLevel = 'info';
|
|
19
|
+
/**
|
|
20
|
+
* Sets the minimum log level. Messages below this level are suppressed.
|
|
21
|
+
*
|
|
22
|
+
* @param level New minimum level.
|
|
23
|
+
*/
|
|
24
|
+
export function setLogLevel(level) {
|
|
25
|
+
currentLevel = level;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Emits a log line to stderr.
|
|
29
|
+
*
|
|
30
|
+
* Fields are serialised as JSON; the message string is NOT interpolated to
|
|
31
|
+
* avoid accidental inclusion of raw data.
|
|
32
|
+
*
|
|
33
|
+
* @param level Severity.
|
|
34
|
+
* @param message Human-readable description. MUST NOT contain token sequences or raw scan data.
|
|
35
|
+
* @param meta Optional additional structured fields (MUST be pre-redacted by caller).
|
|
36
|
+
*/
|
|
37
|
+
export function log(level, message, meta) {
|
|
38
|
+
if (LEVEL_RANK[level] < LEVEL_RANK[currentLevel])
|
|
39
|
+
return;
|
|
40
|
+
const entry = {
|
|
41
|
+
ts: new Date().toISOString(),
|
|
42
|
+
level,
|
|
43
|
+
msg: message,
|
|
44
|
+
};
|
|
45
|
+
if (meta) {
|
|
46
|
+
Object.assign(entry, meta);
|
|
47
|
+
}
|
|
48
|
+
process.stderr.write(JSON.stringify(entry) + '\n');
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,MAAM,UAAU,GAA6B;IAC3C,KAAK,EAAE,CAAC;IACR,IAAI,EAAE,CAAC;IACP,IAAI,EAAE,CAAC;IACP,KAAK,EAAE,CAAC;CACT,CAAC;AAEF,yEAAyE;AACzE,IAAI,YAAY,GAAa,MAAM,CAAC;AAEpC;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,KAAe;IACzC,YAAY,GAAG,KAAK,CAAC;AACvB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,GAAG,CACjB,KAAe,EACf,OAAe,EACf,IAA8B;IAE9B,IAAI,UAAU,CAAC,KAAK,CAAC,GAAG,UAAU,CAAC,YAAY,CAAC;QAAE,OAAO;IAEzD,MAAM,KAAK,GAA4B;QACrC,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QAC5B,KAAK;QACL,GAAG,EAAE,OAAO;KACb,CAAC;IACF,IAAI,IAAI,EAAE,CAAC;QACT,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAC7B,CAAC;IAED,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;AACrD,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State persistence — atomic writes, 0o600 file mode, no sequence storage.
|
|
3
|
+
*
|
|
4
|
+
* SECURITY (RC-1): The `sequence` field is intentionally omitted from the
|
|
5
|
+
* persisted representation. After a server restart, existing token metadata
|
|
6
|
+
* (ID, expiry, leakage history) is recovered, but the invisible Unicode
|
|
7
|
+
* payload is lost. Tokens recovered from disk cannot re-detect their
|
|
8
|
+
* embedded sequence in new data; they are useful only for historical
|
|
9
|
+
* reporting. This is a known limitation — see SECURITY.md.
|
|
10
|
+
*
|
|
11
|
+
* SECURITY (RC-7): All recovered records are validated through manual type
|
|
12
|
+
* guards. Invalid records are discarded with a warning; the server never
|
|
13
|
+
* throws on malformed persistence data.
|
|
14
|
+
*/
|
|
15
|
+
import type { CanaryConfig, SessionState } from './types.js';
|
|
16
|
+
/**
|
|
17
|
+
* Atomically persists session state to disk (sequence field omitted).
|
|
18
|
+
*
|
|
19
|
+
* Writes to a `.tmp` sibling file first, then renames to the target path
|
|
20
|
+
* so the on-disk file is never observed in a partial state. File mode is
|
|
21
|
+
* set to 0o600 (owner read/write only).
|
|
22
|
+
*
|
|
23
|
+
* @param state Current session state.
|
|
24
|
+
* @param config Server configuration (provides `persist_path`).
|
|
25
|
+
*/
|
|
26
|
+
export declare function persistState(state: SessionState, config: CanaryConfig): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Attempts to recover session state from the persistence file.
|
|
29
|
+
*
|
|
30
|
+
* Returns `null` when:
|
|
31
|
+
* - No path is configured.
|
|
32
|
+
* - The file does not exist.
|
|
33
|
+
* - Top-level validation fails (the whole file is malformed).
|
|
34
|
+
*
|
|
35
|
+
* Individual token or event records that fail validation are discarded with
|
|
36
|
+
* a warning rather than crashing the server (RC-7).
|
|
37
|
+
*
|
|
38
|
+
* @param config Server configuration.
|
|
39
|
+
* @returns Recovered `SessionState`, or `null` to start fresh.
|
|
40
|
+
*/
|
|
41
|
+
export declare function recoverState(config: CanaryConfig): Promise<SessionState | null>;
|
|
42
|
+
/**
|
|
43
|
+
* Top-level validation of the raw parsed JSON from the persistence file.
|
|
44
|
+
*
|
|
45
|
+
* @param raw Parsed JSON value.
|
|
46
|
+
*/
|
|
47
|
+
export declare function validatePersistedState(raw: unknown): boolean;
|
|
48
|
+
//# sourceMappingURL=persistence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../src/persistence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EACV,YAAY,EAMZ,YAAY,EACb,MAAM,YAAY,CAAC;AAUpB;;;;;;;;;GASG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,IAAI,CAAC,CAmDf;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAiF9B;AAkCD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAS5D"}
|