@anomira/node-sdk 0.1.6 → 0.1.7

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 CHANGED
@@ -11,9 +11,9 @@ npm install @anomira/node-sdk
11
11
  ## Quick Start
12
12
 
13
13
  ```js
14
- import { SentinelAPI } from "@anomira/node-sdk";
14
+ import { Anomira } from "@anomira/node-sdk";
15
15
 
16
- const sentinel = new SentinelAPI({
16
+ const sentinel = new Anomira({
17
17
  apiKey: process.env.SENTINEL_API_KEY,
18
18
  appId: process.env.SENTINEL_APP_ID,
19
19
  ingestUrl: process.env.SENTINEL_INGEST_URL,
@@ -25,11 +25,11 @@ const sentinel = new SentinelAPI({
25
25
 
26
26
  ```js
27
27
  import express from "express";
28
- import { SentinelAPI } from "@anomira/node-sdk";
28
+ import { Anomira } from "@anomira/node-sdk";
29
29
 
30
30
  const app = express();
31
31
 
32
- const sentinel = new SentinelAPI({
32
+ const sentinel = new Anomira({
33
33
  apiKey: process.env.SENTINEL_API_KEY,
34
34
  appId: process.env.SENTINEL_APP_ID,
35
35
  ingestUrl: process.env.SENTINEL_INGEST_URL,
@@ -56,11 +56,11 @@ app.listen(3000);
56
56
 
57
57
  ```js
58
58
  import Fastify from "fastify";
59
- import { SentinelAPI } from "@anomira/node-sdk";
59
+ import { Anomira } from "@anomira/node-sdk";
60
60
 
61
61
  const app = Fastify();
62
62
 
63
- const sentinel = new SentinelAPI({
63
+ const sentinel = new Anomira({
64
64
  apiKey: process.env.SENTINEL_API_KEY,
65
65
  appId: process.env.SENTINEL_APP_ID,
66
66
  ingestUrl: process.env.SENTINEL_INGEST_URL,
@@ -116,6 +116,28 @@ if (match?.rule.action === "block") {
116
116
  }
117
117
  ```
118
118
 
119
+ ## Shadow Endpoint Detection
120
+
121
+ Register your known API routes on startup so Anomira can flag any undeclared endpoint that appears in live traffic. Endpoints that receive requests but were never declared show up in the **API Surface Map** as shadow endpoints.
122
+
123
+ ```js
124
+ // Call once after your routes are registered
125
+ await sentinel.declareEndpoints([
126
+ { method: "POST", path: "/api/auth/login", auth: false },
127
+ { method: "POST", path: "/api/auth/register", auth: false },
128
+ { method: "GET", path: "/api/users/:id", auth: true },
129
+ { method: "GET", path: "/api/orders", auth: true },
130
+ { method: "POST", path: "/api/orders", auth: true },
131
+ { method: "GET", path: "/api/health", auth: false },
132
+ ]);
133
+ ```
134
+
135
+ Express-style `:param` segments are normalized automatically — `/users/:id` and `/users/:userId` both map to `/users/{id}` to match discovered traffic.
136
+
137
+ Use `auth: false` for public endpoints. Authenticated endpoints default to `auth: true`.
138
+
139
+ Shadow detection from declared endpoints only activates once your org has registered at least one endpoint, so it won't produce noise on fresh deployments before you call this method.
140
+
119
141
  ## Secret Scanner CLI
120
142
 
121
143
  Scan your codebase for hardcoded secrets, API keys, BVN/NIN numbers, and PII before they reach production.
package/dist/cli.cjs CHANGED
@@ -5606,17 +5606,15 @@ var creator = {
5606
5606
 
5607
5607
  // src/sensitive.ts
5608
5608
  var PATTERNS = [
5609
- // ── Cryptographic keys and certificates ───────────────────────────────────
5609
+ // ── Cryptographic private keys ────────────────────────────────────────────
5610
+ // NOTE: -----BEGIN CERTIFICATE----- is intentionally excluded — public
5611
+ // certificates are designed to be public and committing them is correct.
5612
+ // Only PRIVATE keys are dangerous.
5610
5613
  {
5611
5614
  type: "private_key",
5612
5615
  label: "Private Key",
5613
5616
  regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/
5614
5617
  },
5615
- {
5616
- type: "certificate",
5617
- label: "Certificate / Public Key",
5618
- regex: /-----BEGIN CERTIFICATE-----/
5619
- },
5620
5618
  // ── Cloud provider credentials ────────────────────────────────────────────
5621
5619
  {
5622
5620
  // AWS access key ID — highly specific, almost no false positives
@@ -5749,11 +5747,12 @@ var PATTERNS = [
5749
5747
  },
5750
5748
  // ── Nigeria-specific PII ──────────────────────────────────────────────────
5751
5749
  {
5752
- // BVN / NIN: 11 digits, first digit 1-9 (not a phone starting with 0)
5753
- // Require a non-digit boundary on both sides to reduce false positives
5750
+ // BVN / NIN: 11 digits, first digit 1-9 (not a phone number starting with 0)
5751
+ // Exclude: inside URLs (preceded by / : @ - %), hex strings (followed by a-f),
5752
+ // and UUIDs/hashes (surrounded by alphanumeric chars)
5754
5753
  type: "bvn",
5755
5754
  label: "BVN / NIN",
5756
- regex: /(?<!\d)[1-9]\d{10}(?!\d)/
5755
+ regex: /(?<![/\-:@%=a-fA-F\w])[1-9]\d{10}(?![a-fA-F\d])/
5757
5756
  },
5758
5757
  {
5759
5758
  // Nigerian phone numbers: 080x, 081x, 070x, 090x, 091x — or with +234 prefix
@@ -5846,8 +5845,28 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
5846
5845
  ".turbo",
5847
5846
  ".cache",
5848
5847
  "tmp",
5849
- "temp"
5848
+ "temp",
5849
+ // Test directories — skipped by default, use --include-tests to scan them
5850
+ "tests",
5851
+ "test",
5852
+ "__tests__",
5853
+ "spec",
5854
+ "__spec__",
5855
+ "fixtures",
5856
+ "__fixtures__",
5857
+ "mocks",
5858
+ "__mocks__"
5850
5859
  ]);
5860
+ var TEST_FILE_PATTERNS = [
5861
+ ".test.js",
5862
+ ".test.ts",
5863
+ ".test.jsx",
5864
+ ".test.tsx",
5865
+ ".spec.js",
5866
+ ".spec.ts",
5867
+ ".spec.jsx",
5868
+ ".spec.tsx"
5869
+ ];
5851
5870
  function shannonEntropy(str) {
5852
5871
  const freq = {};
5853
5872
  for (const ch of str) freq[ch] = (freq[ch] ?? 0) + 1;
@@ -5879,7 +5898,12 @@ var SECRETLINT_CONFIG = {
5879
5898
  }
5880
5899
  ]
5881
5900
  };
5882
- function shouldScan(filePath) {
5901
+ function isTestFile(filePath) {
5902
+ const base = path2__default.default.basename(filePath);
5903
+ return TEST_FILE_PATTERNS.some((p) => base.endsWith(p));
5904
+ }
5905
+ function shouldScan(filePath, includeTests) {
5906
+ if (!includeTests && isTestFile(filePath)) return false;
5883
5907
  const ext = path2__default.default.extname(filePath);
5884
5908
  const base = path2__default.default.basename(filePath);
5885
5909
  if (base.startsWith(".env")) return true;
@@ -5960,14 +5984,14 @@ async function scanFile(filePath, strict) {
5960
5984
  }
5961
5985
  return violations;
5962
5986
  }
5963
- function* walkDir(dir) {
5987
+ function* walkDir(dir, includeTests) {
5964
5988
  const entries = fs__default.default.readdirSync(dir, { withFileTypes: true });
5965
5989
  for (const entry of entries) {
5966
5990
  if (SKIP_DIRS.has(entry.name)) continue;
5967
5991
  const full = path2__default.default.join(dir, entry.name);
5968
5992
  if (entry.isDirectory()) {
5969
- yield* walkDir(full);
5970
- } else if (entry.isFile() && shouldScan(full)) {
5993
+ yield* walkDir(full, includeTests);
5994
+ } else if (entry.isFile() && shouldScan(full, includeTests)) {
5971
5995
  yield full;
5972
5996
  }
5973
5997
  }
@@ -5979,6 +6003,7 @@ async function main() {
5979
6003
  const quiet = args.includes("--quiet") || args.includes("-q");
5980
6004
  const jsonOut = args.includes("--json");
5981
6005
  const strict = args.includes("--strict");
6006
+ const includeTests = args.includes("--include-tests");
5982
6007
  if (args.includes("--help") || args.includes("-h") || args[0] === "help") {
5983
6008
  console.log(`
5984
6009
  ${c.bold}Anomira Secret Scanner${c.reset}
@@ -5992,10 +6017,11 @@ Detection layers:
5992
6017
  Layer 3 Entropy \u2014 High-entropy string detection (--strict only)
5993
6018
 
5994
6019
  Options:
5995
- --strict Enable entropy analysis (catches unknown secrets, more noise)
5996
- --quiet, -q Only print violations (suppress summary header)
5997
- --json Machine-readable JSON output (for CI pipelines)
5998
- --help, -h Show this help
6020
+ --strict Enable entropy analysis (catches unknown secrets, more noise)
6021
+ --include-tests Also scan test files (*.test.*, *.spec.*, tests/ dirs \u2014 skipped by default)
6022
+ --quiet, -q Only print violations (suppress summary header)
6023
+ --json Machine-readable JSON output (for CI pipelines)
6024
+ --help, -h Show this help
5999
6025
 
6000
6026
  Examples:
6001
6027
  npx @anomira/node-sdk scan ./src
@@ -6019,7 +6045,7 @@ ${c.bold}${c.cyan}Anomira Secret Scanner${c.reset}`);
6019
6045
  console.log(`${c.grey}Layers: secretlint + custom patterns${strict ? " + entropy" : ""}${c.reset}
6020
6046
  `);
6021
6047
  }
6022
- const files = fs__default.default.statSync(target).isDirectory() ? [...walkDir(target)] : [target];
6048
+ const files = fs__default.default.statSync(target).isDirectory() ? [...walkDir(target, includeTests)] : [target];
6023
6049
  const allViolations = [];
6024
6050
  let fileCount = 0;
6025
6051
  for (const file of files) {
package/dist/index.cjs CHANGED
@@ -109,10 +109,10 @@ var EventBuffer = class {
109
109
  }
110
110
  }
111
111
  log(...args) {
112
- if (this.opts.debug) console.log("[SentinelAPI]", ...args);
112
+ if (this.opts.debug) console.log("[Anomira]", ...args);
113
113
  }
114
114
  warn(...args) {
115
- console.warn("[SentinelAPI]", ...args);
115
+ console.warn("[Anomira]", ...args);
116
116
  }
117
117
  };
118
118
  function sleep(ms) {
@@ -199,17 +199,15 @@ async function checkGeoVelocity(userId, ip, tsMs, lookupUrl) {
199
199
 
200
200
  // src/sensitive.ts
201
201
  var PATTERNS = [
202
- // ── Cryptographic keys and certificates ───────────────────────────────────
202
+ // ── Cryptographic private keys ────────────────────────────────────────────
203
+ // NOTE: -----BEGIN CERTIFICATE----- is intentionally excluded — public
204
+ // certificates are designed to be public and committing them is correct.
205
+ // Only PRIVATE keys are dangerous.
203
206
  {
204
207
  type: "private_key",
205
208
  label: "Private Key",
206
209
  regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/
207
210
  },
208
- {
209
- type: "certificate",
210
- label: "Certificate / Public Key",
211
- regex: /-----BEGIN CERTIFICATE-----/
212
- },
213
211
  // ── Cloud provider credentials ────────────────────────────────────────────
214
212
  {
215
213
  // AWS access key ID — highly specific, almost no false positives
@@ -342,11 +340,12 @@ var PATTERNS = [
342
340
  },
343
341
  // ── Nigeria-specific PII ──────────────────────────────────────────────────
344
342
  {
345
- // BVN / NIN: 11 digits, first digit 1-9 (not a phone starting with 0)
346
- // Require a non-digit boundary on both sides to reduce false positives
343
+ // BVN / NIN: 11 digits, first digit 1-9 (not a phone number starting with 0)
344
+ // Exclude: inside URLs (preceded by / : @ - %), hex strings (followed by a-f),
345
+ // and UUIDs/hashes (surrounded by alphanumeric chars)
347
346
  type: "bvn",
348
347
  label: "BVN / NIN",
349
- regex: /(?<!\d)[1-9]\d{10}(?!\d)/
348
+ regex: /(?<![/\-:@%=a-fA-F\w])[1-9]\d{10}(?![a-fA-F\d])/
350
349
  },
351
350
  {
352
351
  // Nigerian phone numbers: 080x, 081x, 070x, 090x, 091x — or with +234 prefix
@@ -413,7 +412,7 @@ var DEFAULT_INGEST_URL = "https://ingest.anomira.io/v1/events";
413
412
  var DEFAULT_BATCH_SIZE = 100;
414
413
  var DEFAULT_FLUSH_MS = 5e3;
415
414
  var DEFAULT_MAX_RETRIES = 3;
416
- var SentinelClient = class {
415
+ var AnomiraClient = class {
417
416
  constructor(config) {
418
417
  this.logBuffer = [];
419
418
  this.logFlushTimer = null;
@@ -512,7 +511,7 @@ var SentinelClient = class {
512
511
  console[method] = (...args) => {
513
512
  original(...args);
514
513
  const first = args[0];
515
- if (typeof first === "string" && first.startsWith("[SentinelAPI]")) return;
514
+ if (typeof first === "string" && first.startsWith("[Anomira]")) return;
516
515
  const message = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
517
516
  const ctx = requestContext.getStore();
518
517
  this.log(level, message, {
@@ -536,7 +535,7 @@ var SentinelClient = class {
536
535
  const data = await res.json();
537
536
  this.blockedIpCache = new Set(data.ips ?? []);
538
537
  if (this.config.debug && this.blockedIpCache.size > 0) {
539
- this._origLog(`[SentinelAPI] blocklist refreshed \u2014 ${this.blockedIpCache.size} blocked IPs`);
538
+ this._origLog(`[Anomira] blocklist refreshed \u2014 ${this.blockedIpCache.size} blocked IPs`);
540
539
  }
541
540
  } catch {
542
541
  }
@@ -564,7 +563,7 @@ var SentinelClient = class {
564
563
  })() : void 0
565
564
  }));
566
565
  if (this.config.debug && this.compiledRules.length > 0) {
567
- this._origLog(`[SentinelAPI] firewall rules refreshed \u2014 ${this.compiledRules.length} active rules`);
566
+ this._origLog(`[Anomira] firewall rules refreshed \u2014 ${this.compiledRules.length} active rules`);
568
567
  }
569
568
  } catch {
570
569
  }
@@ -615,7 +614,7 @@ var SentinelClient = class {
615
614
  signal: AbortSignal.timeout(8e3)
616
615
  });
617
616
  if (this.config.debug) {
618
- this._origLog(`[SentinelAPI] [logs] \u2705 sent ${batch.length} log entries (${res.status})`);
617
+ this._origLog(`[Anomira] [logs] \u2705 sent ${batch.length} log entries (${res.status})`);
619
618
  }
620
619
  } catch {
621
620
  this.logBuffer.unshift(...batch);
@@ -634,24 +633,24 @@ var SentinelClient = class {
634
633
  signal: AbortSignal.timeout(5e3)
635
634
  });
636
635
  if (res.status >= 300 && res.status < 400) {
637
- this._origWarn(`[SentinelAPI] \u274C Wrong ingest URL \u2014 got redirect to ${res.headers.get("location")}. Check SENTINEL_INGEST_URL.`);
636
+ this._origWarn(`[Anomira] \u274C Wrong ingest URL \u2014 got redirect to ${res.headers.get("location")}. Check SENTINEL_INGEST_URL.`);
638
637
  return;
639
638
  }
640
639
  if (res.ok) {
641
- this._origLog(`[SentinelAPI] \u2705 Connected (appId: ${this.config.appId.slice(0, 8)}\u2026)`);
640
+ this._origLog(`[Anomira] \u2705 Connected (appId: ${this.config.appId.slice(0, 8)}\u2026)`);
642
641
  return;
643
642
  }
644
643
  if (res.status === 401) {
645
- this._origWarn("[SentinelAPI] \u274C Invalid API key \u2014 check your SENTINEL_API_KEY");
644
+ this._origWarn("[Anomira] \u274C Invalid API key \u2014 check your SENTINEL_API_KEY");
646
645
  return;
647
646
  }
648
647
  if (res.status === 403) {
649
- this._origWarn("[SentinelAPI] \u274C App not found or appId mismatch \u2014 check your SENTINEL_APP_ID");
648
+ this._origWarn("[Anomira] \u274C App not found or appId mismatch \u2014 check your SENTINEL_APP_ID");
650
649
  return;
651
650
  }
652
- this._origWarn(`[SentinelAPI] \u26A0\uFE0F Ingest returned HTTP ${res.status} \u2014 check your configuration`);
651
+ this._origWarn(`[Anomira] \u26A0\uFE0F Ingest returned HTTP ${res.status} \u2014 check your configuration`);
653
652
  } catch {
654
- this._origWarn("[SentinelAPI] \u26A0\uFE0F Could not reach ingest endpoint \u2014 check SENTINEL_INGEST_URL (current: " + this.config.ingestUrl + ")");
653
+ this._origWarn("[Anomira] \u26A0\uFE0F Could not reach ingest endpoint \u2014 check SENTINEL_INGEST_URL (current: " + this.config.ingestUrl + ")");
655
654
  }
656
655
  }
657
656
  // ─── Public API ────────────────────────────────────────────────────────────
@@ -748,7 +747,7 @@ var SentinelClient = class {
748
747
  });
749
748
  }
750
749
  /**
751
- * Send a structured log entry to the SentinelAPI Logs dashboard.
750
+ * Send a structured log entry to the Anomira Logs dashboard.
752
751
  *
753
752
  * ```ts
754
753
  * sentinel.log("info", "User registered", { userId: user.id });
@@ -764,16 +763,47 @@ var SentinelClient = class {
764
763
  rest["sensitiveLeaks"] = leaks.map((l) => l.type);
765
764
  if (this.config.debug) {
766
765
  this._origWarn(
767
- `[SentinelAPI] \u26A0\uFE0F Sensitive data in log (${leaks.map((l) => l.label).join(", ")}): "${message.slice(0, 60)}\u2026"`
766
+ `[Anomira] \u26A0\uFE0F Sensitive data in log (${leaks.map((l) => l.label).join(", ")}): "${message.slice(0, 60)}\u2026"`
768
767
  );
769
768
  }
770
769
  }
771
770
  this.logBuffer.push({ level, service: service ?? this.config.service, message, meta: rest, ts: Date.now() });
772
771
  if (this.config.debug && leaks.length === 0) {
773
- this._origLog(`[SentinelAPI] log:${level} ${message}`);
772
+ this._origLog(`[Anomira] log:${level} ${message}`);
774
773
  }
775
774
  if (this.logBuffer.length >= 50) void this.#flushLogs();
776
775
  }
776
+ /**
777
+ * Declare your API's known endpoints so Anomira can flag undiscovered
778
+ * traffic as shadow endpoints.
779
+ *
780
+ * Call this once on startup after your routes are registered:
781
+ *
782
+ * ```ts
783
+ * await sentinel.declareEndpoints([
784
+ * { method: "GET", path: "/api/users/:id", auth: true },
785
+ * { method: "POST", path: "/api/orders", auth: true },
786
+ * { method: "GET", path: "/api/health", auth: false },
787
+ * ]);
788
+ * ```
789
+ */
790
+ async declareEndpoints(endpoints) {
791
+ if (this.disabled || endpoints.length === 0) return;
792
+ const url = this.config.ingestUrl.replace(/\/v1\/events$/, "/v1/declare-endpoints");
793
+ try {
794
+ await fetch(url, {
795
+ method: "POST",
796
+ headers: {
797
+ Authorization: `Bearer ${this.config.apiKey}`,
798
+ "Content-Type": "application/json",
799
+ "User-Agent": `@anomira/node-sdk/0.1.0`
800
+ },
801
+ body: JSON.stringify({ appId: this.config.appId, endpoints }),
802
+ signal: AbortSignal.timeout(1e4)
803
+ });
804
+ } catch {
805
+ }
806
+ }
777
807
  /**
778
808
  * Flush all pending events immediately.
779
809
  * Useful before a graceful shutdown outside of the process lifecycle hooks.
@@ -1114,10 +1144,11 @@ function createFastifyPlugin2(client) {
1114
1144
  };
1115
1145
  }
1116
1146
 
1147
+ exports.Anomira = AnomiraClient;
1117
1148
  exports.EventName = EventName;
1118
- exports.SentinelAPI = SentinelClient;
1149
+ exports.SentinelAPI = AnomiraClient;
1119
1150
  exports.createExpressMiddleware = createExpressMiddleware2;
1120
1151
  exports.createFastifyPlugin = createFastifyPlugin2;
1121
- exports.default = SentinelClient;
1152
+ exports.default = AnomiraClient;
1122
1153
  //# sourceMappingURL=index.cjs.map
1123
1154
  //# sourceMappingURL=index.cjs.map