@anomira/node-sdk 0.1.3 → 0.1.5

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/dist/index.js CHANGED
@@ -195,18 +195,124 @@ async function checkGeoVelocity(userId, ip, tsMs, lookupUrl) {
195
195
 
196
196
  // src/sensitive.ts
197
197
  var PATTERNS = [
198
+ // ── Cryptographic keys and certificates ───────────────────────────────────
198
199
  {
199
- // "password: abc123", "pwd=secret", or standalone "password123"
200
- type: "password",
201
- label: "Password / Secret",
202
- regex: /\bpassword\w*|\b(?:passwd|pwd|pass|secret|credentials?)\s*[:=]\s*\S+/i
200
+ type: "private_key",
201
+ label: "Private Key",
202
+ regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/
203
203
  },
204
204
  {
205
- // Nigerian BVN / NIN — exactly 11 digits, not a phone number (phones start with 0)
206
- type: "bvn",
207
- label: "BVN / NIN",
208
- regex: /\b[1-9]\d{10}\b/
205
+ type: "certificate",
206
+ label: "Certificate / Public Key",
207
+ regex: /-----BEGIN CERTIFICATE-----/
208
+ },
209
+ // ── Cloud provider credentials ────────────────────────────────────────────
210
+ {
211
+ // AWS access key ID — highly specific, almost no false positives
212
+ type: "aws_key",
213
+ label: "AWS Access Key",
214
+ regex: /\bAKIA[0-9A-Z]{16}\b/
215
+ },
216
+ {
217
+ // AWS secret access key — 40-char base64 string after keyword
218
+ type: "aws_secret",
219
+ label: "AWS Secret Key",
220
+ regex: /\b(?:aws[_-]?secret|AWS_SECRET_ACCESS_KEY)\s*[:=]\s*[A-Za-z0-9+/]{40}\b/i
221
+ },
222
+ {
223
+ // Google API key
224
+ type: "google_key",
225
+ label: "Google API Key",
226
+ regex: /\bAIza[0-9A-Za-z\-_]{35}\b/
227
+ },
228
+ {
229
+ // Google OAuth client secret
230
+ type: "google_oauth",
231
+ label: "Google OAuth Secret",
232
+ regex: /\bGOCSP[A-Za-z0-9\-_]{28}\b/
233
+ },
234
+ {
235
+ // Firebase server key
236
+ type: "firebase_key",
237
+ label: "Firebase Server Key",
238
+ regex: /\bAAAA[A-Za-z0-9_-]{7}:[A-Za-z0-9_-]{140}\b/
209
239
  },
240
+ {
241
+ // Azure storage/connection string
242
+ type: "azure_key",
243
+ label: "Azure Key",
244
+ regex: /\bDefaultEndpointsProtocol=https;AccountName=[^;]+;AccountKey=[A-Za-z0-9+/=]{88}/
245
+ },
246
+ // ── Source control & CI tokens ────────────────────────────────────────────
247
+ {
248
+ // GitHub personal access tokens (classic and fine-grained)
249
+ type: "github_token",
250
+ label: "GitHub Token",
251
+ regex: /\b(?:ghp|gho|ghu|ghs|ghr|github_pat)_[A-Za-z0-9_]{36,255}\b/
252
+ },
253
+ {
254
+ // GitLab personal/project/group tokens
255
+ type: "gitlab_token",
256
+ label: "GitLab Token",
257
+ regex: /\bglpat-[A-Za-z0-9\-_]{20}\b/
258
+ },
259
+ {
260
+ // NPM access tokens
261
+ type: "npm_token",
262
+ label: "NPM Token",
263
+ regex: /\bnpm_[A-Za-z0-9]{36}\b/
264
+ },
265
+ // ── Payment providers ─────────────────────────────────────────────────────
266
+ {
267
+ // Stripe — secret, restricted, webhook keys
268
+ type: "stripe_key",
269
+ label: "Stripe Key",
270
+ regex: /\b(?:sk|rk|whsec)_(?:live|test)_[A-Za-z0-9]{24,}\b/
271
+ },
272
+ {
273
+ // Paystack secret/public keys
274
+ type: "paystack_key",
275
+ label: "Paystack Key",
276
+ regex: /\b(?:sk|pk)_(?:live|test)_[A-Za-z0-9]{40,}\b/
277
+ },
278
+ {
279
+ // Card PANs: Visa (4), Mastercard (51-55), Amex (34/37), Discover (6011/65)
280
+ type: "card_pan",
281
+ label: "Card PAN",
282
+ regex: /\b(?:4[0-9]{15}|5[1-5][0-9]{14}|3[47][0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12})\b/
283
+ },
284
+ // ── Communication & messaging ─────────────────────────────────────────────
285
+ {
286
+ // Slack bot/user/app tokens
287
+ type: "slack_token",
288
+ label: "Slack Token",
289
+ regex: /\bxox[baprs]-[0-9A-Za-z]{10,48}\b/
290
+ },
291
+ {
292
+ // Slack webhook URL
293
+ type: "slack_webhook",
294
+ label: "Slack Webhook URL",
295
+ regex: /https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+/
296
+ },
297
+ {
298
+ // Twilio account SID and auth token
299
+ type: "twilio",
300
+ label: "Twilio Credential",
301
+ regex: /\bAC[a-z0-9]{32}\b|\bSK[a-z0-9]{32}\b/
302
+ },
303
+ {
304
+ // SendGrid / Brevo / Mailgun API keys
305
+ type: "email_provider_key",
306
+ label: "Email Provider API Key",
307
+ regex: /\bSG\.[A-Za-z0-9._-]{66}\b|\bkey-[0-9a-zA-Z]{32}\b/
308
+ },
309
+ // ── Database connection strings with embedded credentials ─────────────────
310
+ {
311
+ type: "db_connection",
312
+ label: "Database Connection String",
313
+ regex: /(?:postgresql|postgres|mysql|mongodb(?:\+srv)?|redis|amqp(?:s)?):\/\/[^:]+:[^@\s]{3,}@/i
314
+ },
315
+ // ── Auth tokens ───────────────────────────────────────────────────────────
210
316
  {
211
317
  // JWT — three base64url segments separated by dots
212
318
  type: "jwt",
@@ -214,36 +320,49 @@ var PATTERNS = [
214
320
  regex: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/
215
321
  },
216
322
  {
217
- // API keys: sk_live_*, pk_live_*, or key/token/bearer label with long value
323
+ // Generic API key / token assigned to a keyword (must have 20+ char value)
218
324
  type: "api_key",
219
325
  label: "API Key / Token",
220
- regex: /\b(?:sk|pk|rk)[-_](?:live|test|prod|secret)[-_][A-Za-z0-9]{16,}|\b(?:token|bearer|access_token|api_key)\s*[:=]\s*[A-Za-z0-9_./-]{20,}/i
326
+ regex: /\b(?:api[_-]?key|access[_-]?token|auth[_-]?token|bearer|client[_-]?secret)\s*[:=]\s*["']?[A-Za-z0-9_.\/+\-]{20,}["']?/i
221
327
  },
328
+ // ── Password fields ───────────────────────────────────────────────────────
222
329
  {
223
- // Card PANs: Visa (4), Mastercard (51-55), Amex (34/37), Discover (6011/65)
224
- type: "card_pan",
225
- label: "Card PAN",
226
- regex: /\b(?:4[0-9]{15}|5[1-5][0-9]{14}|3[47][0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12})\b/
330
+ // Password/secret keyword immediately followed by a non-placeholder value
331
+ // Avoids false positives on "password: ${PASSWORD}" or "password: xxxx"
332
+ type: "password",
333
+ label: "Password / Secret",
334
+ regex: /\b(?:password|passwd|pwd|pass|secret|credentials?)\s*[:=]\s*["']?(?!\$\{)[^\s"'$]{6,}["']?/i
335
+ },
336
+ // ── Nigeria-specific PII ──────────────────────────────────────────────────
337
+ {
338
+ // BVN / NIN: 11 digits, first digit 1-9 (not a phone starting with 0)
339
+ // Require a non-digit boundary on both sides to reduce false positives
340
+ type: "bvn",
341
+ label: "BVN / NIN",
342
+ regex: /(?<!\d)[1-9]\d{10}(?!\d)/
227
343
  },
228
344
  {
229
345
  // Nigerian phone numbers: 080x, 081x, 070x, 090x, 091x — or with +234 prefix
230
346
  type: "ng_phone",
231
- label: "Phone Number",
347
+ label: "Nigerian Phone Number",
232
348
  regex: /\b(?:\+?234|0)(?:7[0-9]|8[0-1]|9[0-1])\d{8}\b/
233
349
  },
350
+ // ── PII in credential context ─────────────────────────────────────────────
234
351
  {
235
- // Email address appearing alongside other content possible credential combo
236
- // e.g. "john@test.com password123"
352
+ // Email only flagged when adjacent to a password/credential keyword
353
+ // Prevents false positives on normal email references in code
237
354
  type: "email_credential",
238
- label: "Email in Log",
239
- regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/
355
+ label: "Email + Password Combo",
356
+ regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\s+(?:password|passwd|pwd|secret|pass)\s*[:=]?\s*\S+/i
240
357
  }
241
358
  ];
242
359
  function scanForLeaks(message) {
360
+ const seen = /* @__PURE__ */ new Set();
243
361
  const found = [];
244
362
  for (const p of PATTERNS) {
245
363
  p.regex.lastIndex = 0;
246
- if (p.regex.test(message)) {
364
+ if (p.regex.test(message) && !seen.has(p.type)) {
365
+ seen.add(p.type);
247
366
  found.push({ type: p.type, label: p.label });
248
367
  }
249
368
  }
@@ -293,6 +412,8 @@ var SentinelClient = class {
293
412
  this.logFlushTimer = null;
294
413
  this.blocklistTimer = null;
295
414
  this.firewallTimer = null;
415
+ /** True when credentials are missing — all operations become no-ops. */
416
+ this.disabled = false;
296
417
  /** In-process cache of blocked IPs — refreshed every 60 s from the ingest server. */
297
418
  this.blockedIpCache = /* @__PURE__ */ new Set();
298
419
  /** In-process cache of firewall rules with pre-compiled regex — refreshed every 60 s. */
@@ -301,8 +422,28 @@ var SentinelClient = class {
301
422
  this._origLog = console.log.bind(console);
302
423
  this._origWarn = console.warn.bind(console);
303
424
  this._origError = console.error.bind(console);
304
- if (!config.apiKey) throw new Error("[SentinelAPI] apiKey is required");
305
- if (!config.appId) throw new Error("[SentinelAPI] appId is required");
425
+ if (!config.apiKey || !config.appId) {
426
+ const missing = [!config.apiKey && "apiKey", !config.appId && "appId"].filter(Boolean).join(", ");
427
+ console.warn(`[Anomira] SDK disabled \u2014 missing config: ${missing}. Set SENTINEL_API_KEY and SENTINEL_APP_ID to enable monitoring.`);
428
+ this.disabled = true;
429
+ this.config = {
430
+ apiKey: "",
431
+ appId: "",
432
+ ingestUrl: DEFAULT_INGEST_URL,
433
+ geoLookupUrl: "",
434
+ maxBatchSize: DEFAULT_BATCH_SIZE,
435
+ flushIntervalMs: DEFAULT_FLUSH_MS,
436
+ maxRetries: DEFAULT_MAX_RETRIES,
437
+ debug: false,
438
+ captureConsole: false,
439
+ service: "app",
440
+ getUserId: defaultGetUserId,
441
+ getIp: defaultGetIp,
442
+ detect: { bruteForce: true, rateAbuse: true, pathTraversal: true, xss: true, scanDetection: true, geoVelocity: true }
443
+ };
444
+ this.buffer = new EventBuffer({ appId: "", apiKey: "", ingestUrl: DEFAULT_INGEST_URL, maxBatchSize: 0, flushIntervalMs: 999999999, maxRetries: 0, debug: false });
445
+ return;
446
+ }
306
447
  this.config = {
307
448
  apiKey: config.apiKey,
308
449
  appId: config.appId,
@@ -521,6 +662,7 @@ var SentinelClient = class {
521
662
  */
522
663
  /** Returns true if the IP is in the current blocked list. Synchronous — no network call. */
523
664
  isBlocked(ip) {
665
+ if (this.disabled) return false;
524
666
  return this.blockedIpCache.has(ip);
525
667
  }
526
668
  /** Evaluate firewall rules against a request. Returns the matched rule or null. Synchronous. */
@@ -528,6 +670,7 @@ var SentinelClient = class {
528
670
  return this.#matchFirewallRule(req);
529
671
  }
530
672
  track(eventName, data) {
673
+ if (this.disabled) return;
531
674
  const event = {
532
675
  name: eventName,
533
676
  ts: Date.now(),
@@ -550,6 +693,7 @@ var SentinelClient = class {
550
693
  * ```
551
694
  */
552
695
  async trackLogin(data) {
696
+ if (this.disabled) return;
553
697
  const tsMs = Date.now();
554
698
  this.track(EventName.LOGIN_SUCCESS, { ...data, meta: { ...data.meta } });
555
699
  if (!this.config.detect.geoVelocity) return;
@@ -589,6 +733,7 @@ var SentinelClient = class {
589
733
  * ```
590
734
  */
591
735
  trackPhoneAuth(data) {
736
+ if (this.disabled) return;
592
737
  this.track(EventName.PHONE_AUTH, {
593
738
  ip: data.ip,
594
739
  userId: data.userId,
@@ -605,6 +750,7 @@ var SentinelClient = class {
605
750
  * ```
606
751
  */
607
752
  log(level, message, meta) {
753
+ if (this.disabled) return;
608
754
  const { service, ...rest } = meta ?? {};
609
755
  const leaks = scanForLeaks(message);
610
756
  if (leaks.length > 0) {
@@ -626,6 +772,7 @@ var SentinelClient = class {
626
772
  * Useful before a graceful shutdown outside of the process lifecycle hooks.
627
773
  */
628
774
  async flush() {
775
+ if (this.disabled) return;
629
776
  await Promise.all([this.buffer.flush(), this.#flushLogs()]);
630
777
  }
631
778
  /**