securenow 8.11.2 → 8.11.3

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/TELEMETRY-SQL.md CHANGED
@@ -81,10 +81,15 @@ Event properties are stored in the logs table:
81
81
  | `userId` | `attributes_string['enduser.id']` |
82
82
  | `sessionId` | `attributes_string['session.id']` |
83
83
  | `ip` | `attributes_string['http.client_ip']` |
84
- | `userAgent` | `attributes_string['http.user_agent']` |
84
+ | `userAgent` | `attributes_string['user_agent.original']` (HTTP spans use `http.user_agent`) |
85
85
  | String custom attribute | `attributes_string['<key>']` |
86
86
  | Numeric custom attribute | `attributes_number['<key>']` |
87
87
  | Boolean custom attribute | `attributes_bool['<key>']` |
88
+ | ASN of `ip` (when the instance has ASN enrichment) | `attributes_string['client.asn']`, `attributes_string['client.as_org']` |
89
+
90
+ Custom attribute keys that start with a reserved prefix — `event.`, `enduser.`, `session.`, `http.`, `service.`, `securenow.`, `client.` — are stored as `attr.<key>`. Name custom attributes without those prefixes (`auth_method`, `device_hash`, …). Events carry the timestamp the sender supplied; there is no separate ingest time and no event ID.
91
+
92
+ The logs `timestamp` is UInt64 nanoseconds: filter with `timestamp >= toUnixTimestamp64Nano(now64() - INTERVAL 15 MINUTE)`. `timestamp >= now() - INTERVAL …` is accepted by ClickHouse but always true on this table, so the rule would scan the whole retention period — the API rejects it on rule create/update.
88
93
 
89
94
  Never send raw passwords, tokens, OTPs, session secrets, email addresses, phone numbers, or other unnecessary personal data. Hash destinations when correlation is required.
90
95
 
package/cidr.js CHANGED
@@ -1,83 +1,83 @@
1
- 'use strict';
2
-
3
- /**
4
- * Bitmask-based CIDR matching. No regex on user input — prevents ReDoS.
5
- * All operations use unsigned 32-bit integers for IPv4 addresses.
6
- */
7
-
8
- function ipToInt(ip) {
9
- const parts = ip.split('.');
10
- if (parts.length !== 4) return null;
11
- let result = 0;
12
- for (let i = 0; i < 4; i++) {
13
- const n = parseInt(parts[i], 10);
14
- if (isNaN(n) || n < 0 || n > 255) return null;
15
- result = (result << 8) + n;
16
- }
17
- return result >>> 0;
18
- }
19
-
20
- function parseCidr(cidr) {
21
- const slash = cidr.indexOf('/');
22
- if (slash === -1) return null;
23
- const ip = cidr.slice(0, slash);
24
- const bits = parseInt(cidr.slice(slash + 1), 10);
25
- if (isNaN(bits) || bits < 0 || bits > 32) return null;
26
- const network = ipToInt(ip);
27
- if (network === null) return null;
28
- const mask = bits === 0 ? 0 : (~((1 << (32 - bits)) - 1)) >>> 0;
29
- return { network: (network & mask) >>> 0, mask };
30
- }
31
-
32
- function matchesCidr(ipInt, cidrEntry) {
33
- return ((ipInt & cidrEntry.mask) >>> 0) === cidrEntry.network;
34
- }
35
-
36
- /**
37
- * Create a matcher from a list of IPs and CIDRs.
38
- * Returns { isBlocked(ip), stats() }.
39
- * Exact IPs use a Set for O(1) lookup; CIDRs use array scan (typically < 100 entries).
40
- */
41
- function createMatcher(ipList) {
42
- const exactSet = new Set();
43
- const cidrRanges = [];
44
-
45
- for (const entry of ipList) {
46
- const trimmed = (entry || '').trim();
47
- if (!trimmed) continue;
48
-
49
- if (trimmed.includes('/')) {
50
- const parsed = parseCidr(trimmed);
51
- if (parsed) cidrRanges.push(parsed);
52
- } else {
53
- const normalized = trimmed.replace(/^::ffff:/, '');
54
- exactSet.add(normalized);
55
- }
56
- }
57
-
58
- function isBlocked(ip) {
59
- if (!ip) return false;
60
- const normalized = ip.replace(/^::ffff:/, '');
61
-
62
- if (exactSet.has(normalized)) return true;
63
-
64
- if (cidrRanges.length > 0) {
65
- const ipInt = ipToInt(normalized);
66
- if (ipInt !== null) {
67
- for (const cidr of cidrRanges) {
68
- if (matchesCidr(ipInt, cidr)) return true;
69
- }
70
- }
71
- }
72
-
73
- return false;
74
- }
75
-
76
- function stats() {
77
- return { exact: exactSet.size, cidr: cidrRanges.length, total: exactSet.size + cidrRanges.length };
78
- }
79
-
80
- return { isBlocked, stats };
81
- }
82
-
83
- module.exports = { ipToInt, parseCidr, matchesCidr, createMatcher };
1
+ 'use strict';
2
+
3
+ /**
4
+ * Bitmask-based CIDR matching. No regex on user input — prevents ReDoS.
5
+ * All operations use unsigned 32-bit integers for IPv4 addresses.
6
+ */
7
+
8
+ function ipToInt(ip) {
9
+ const parts = ip.split('.');
10
+ if (parts.length !== 4) return null;
11
+ let result = 0;
12
+ for (let i = 0; i < 4; i++) {
13
+ const n = parseInt(parts[i], 10);
14
+ if (isNaN(n) || n < 0 || n > 255) return null;
15
+ result = (result << 8) + n;
16
+ }
17
+ return result >>> 0;
18
+ }
19
+
20
+ function parseCidr(cidr) {
21
+ const slash = cidr.indexOf('/');
22
+ if (slash === -1) return null;
23
+ const ip = cidr.slice(0, slash);
24
+ const bits = parseInt(cidr.slice(slash + 1), 10);
25
+ if (isNaN(bits) || bits < 0 || bits > 32) return null;
26
+ const network = ipToInt(ip);
27
+ if (network === null) return null;
28
+ const mask = bits === 0 ? 0 : (~((1 << (32 - bits)) - 1)) >>> 0;
29
+ return { network: (network & mask) >>> 0, mask };
30
+ }
31
+
32
+ function matchesCidr(ipInt, cidrEntry) {
33
+ return ((ipInt & cidrEntry.mask) >>> 0) === cidrEntry.network;
34
+ }
35
+
36
+ /**
37
+ * Create a matcher from a list of IPs and CIDRs.
38
+ * Returns { isBlocked(ip), stats() }.
39
+ * Exact IPs use a Set for O(1) lookup; CIDRs use array scan (typically < 100 entries).
40
+ */
41
+ function createMatcher(ipList) {
42
+ const exactSet = new Set();
43
+ const cidrRanges = [];
44
+
45
+ for (const entry of ipList) {
46
+ const trimmed = (entry || '').trim();
47
+ if (!trimmed) continue;
48
+
49
+ if (trimmed.includes('/')) {
50
+ const parsed = parseCidr(trimmed);
51
+ if (parsed) cidrRanges.push(parsed);
52
+ } else {
53
+ const normalized = trimmed.replace(/^::ffff:/, '');
54
+ exactSet.add(normalized);
55
+ }
56
+ }
57
+
58
+ function isBlocked(ip) {
59
+ if (!ip) return false;
60
+ const normalized = ip.replace(/^::ffff:/, '');
61
+
62
+ if (exactSet.has(normalized)) return true;
63
+
64
+ if (cidrRanges.length > 0) {
65
+ const ipInt = ipToInt(normalized);
66
+ if (ipInt !== null) {
67
+ for (const cidr of cidrRanges) {
68
+ if (matchesCidr(ipInt, cidr)) return true;
69
+ }
70
+ }
71
+ }
72
+
73
+ return false;
74
+ }
75
+
76
+ function stats() {
77
+ return { exact: exactSet.size, cidr: cidrRanges.length, total: exactSet.size + cidrRanges.length };
78
+ }
79
+
80
+ return { isBlocked, stats };
81
+ }
82
+
83
+ module.exports = { ipToInt, parseCidr, matchesCidr, createMatcher };
package/cli/client.js CHANGED
@@ -1,55 +1,55 @@
1
- 'use strict';
2
-
3
- const https = require('https');
4
- const http = require('http');
5
- const { URL } = require('url');
6
- const config = require('./config');
7
- const ui = require('./ui');
8
-
9
- function buildQueryString(params) {
10
- if (!params) return '';
11
- const entries = Object.entries(params).filter(([, v]) => v != null && v !== '');
12
- if (!entries.length) return '';
13
- return '?' + entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&');
14
- }
15
-
16
- function request(method, endpoint, { body, query, token, raw } = {}) {
17
- const baseUrl = config.getApiUrl();
18
- const qs = buildQueryString(query);
19
- const urlStr = `${baseUrl}/api${endpoint}${qs}`;
20
- const url = new URL(urlStr);
21
- const mod = url.protocol === 'https:' ? https : http;
22
-
23
- const authToken = token || config.getToken();
24
-
25
- return new Promise((resolve, reject) => {
26
- const options = {
27
- hostname: url.hostname,
28
- port: url.port || (url.protocol === 'https:' ? 443 : 80),
29
- path: url.pathname + url.search,
30
- method,
31
- headers: { 'Content-Type': 'application/json', 'User-Agent': 'securenow-cli' },
32
- };
33
-
34
- if (authToken) {
35
- options.headers['Authorization'] = `Bearer ${authToken}`;
36
- }
37
-
38
- const req = mod.request(options, (res) => {
39
- let data = '';
40
- res.on('data', (chunk) => (data += chunk));
41
- res.on('end', () => {
42
- if (raw) {
43
- return resolve({ status: res.statusCode, headers: res.headers, body: data });
44
- }
45
-
46
- let parsed;
47
- try {
48
- parsed = JSON.parse(data);
49
- } catch {
50
- parsed = data;
51
- }
52
-
1
+ 'use strict';
2
+
3
+ const https = require('https');
4
+ const http = require('http');
5
+ const { URL } = require('url');
6
+ const config = require('./config');
7
+ const ui = require('./ui');
8
+
9
+ function buildQueryString(params) {
10
+ if (!params) return '';
11
+ const entries = Object.entries(params).filter(([, v]) => v != null && v !== '');
12
+ if (!entries.length) return '';
13
+ return '?' + entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&');
14
+ }
15
+
16
+ function request(method, endpoint, { body, query, token, raw } = {}) {
17
+ const baseUrl = config.getApiUrl();
18
+ const qs = buildQueryString(query);
19
+ const urlStr = `${baseUrl}/api${endpoint}${qs}`;
20
+ const url = new URL(urlStr);
21
+ const mod = url.protocol === 'https:' ? https : http;
22
+
23
+ const authToken = token || config.getToken();
24
+
25
+ return new Promise((resolve, reject) => {
26
+ const options = {
27
+ hostname: url.hostname,
28
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
29
+ path: url.pathname + url.search,
30
+ method,
31
+ headers: { 'Content-Type': 'application/json', 'User-Agent': 'securenow-cli' },
32
+ };
33
+
34
+ if (authToken) {
35
+ options.headers['Authorization'] = `Bearer ${authToken}`;
36
+ }
37
+
38
+ const req = mod.request(options, (res) => {
39
+ let data = '';
40
+ res.on('data', (chunk) => (data += chunk));
41
+ res.on('end', () => {
42
+ if (raw) {
43
+ return resolve({ status: res.statusCode, headers: res.headers, body: data });
44
+ }
45
+
46
+ let parsed;
47
+ try {
48
+ parsed = JSON.parse(data);
49
+ } catch {
50
+ parsed = data;
51
+ }
52
+
53
53
  if (res.statusCode === 401) {
54
54
  reject(new CLIError('Admin auth is missing or expired. Run `securenow admin login` to re-authenticate. Runtime app credentials are unrelated.', 401));
55
55
  return;
@@ -59,58 +59,58 @@ function request(method, endpoint, { body, query, token, raw } = {}) {
59
59
  reject(new CLIError(`${msg}. Admin auth is connected, but this user/plan may lack permission for this control-plane operation. Runtime app connection is unrelated.`, 403));
60
60
  return;
61
61
  }
62
- if (res.statusCode >= 400) {
63
- const msg = parsed?.error || parsed?.message || `Request failed (HTTP ${res.statusCode})`;
64
- const details = parsed?.details || parsed?.unauthorizedKeys;
65
- const err = new CLIError(details ? `${msg} — ${details}` : msg, res.statusCode);
66
- reject(err);
67
- return;
68
- }
69
-
70
- resolve(parsed);
71
- });
72
- });
73
-
74
- req.on('error', (err) => {
75
- if (err.code === 'ECONNREFUSED') {
76
- reject(new CLIError(`Cannot connect to ${baseUrl}. Is the API server running?`));
77
- } else if (err.code === 'ENOTFOUND') {
78
- reject(new CLIError(`Cannot resolve ${url.hostname}. Check your internet connection.`));
79
- } else {
80
- reject(new CLIError(`Network error: ${err.message}`));
81
- }
82
- });
83
-
84
- if (body) {
85
- req.write(JSON.stringify(body));
86
- }
87
- req.end();
88
- });
89
- }
90
-
91
- class CLIError extends Error {
92
- constructor(message, statusCode) {
93
- super(message);
94
- this.name = 'CLIError';
95
- this.statusCode = statusCode;
96
- }
97
- }
98
-
99
- function requireAuth() {
62
+ if (res.statusCode >= 400) {
63
+ const msg = parsed?.error || parsed?.message || `Request failed (HTTP ${res.statusCode})`;
64
+ const details = parsed?.details || parsed?.unauthorizedKeys;
65
+ const err = new CLIError(details ? `${msg} — ${details}` : msg, res.statusCode);
66
+ reject(err);
67
+ return;
68
+ }
69
+
70
+ resolve(parsed);
71
+ });
72
+ });
73
+
74
+ req.on('error', (err) => {
75
+ if (err.code === 'ECONNREFUSED') {
76
+ reject(new CLIError(`Cannot connect to ${baseUrl}. Is the API server running?`));
77
+ } else if (err.code === 'ENOTFOUND') {
78
+ reject(new CLIError(`Cannot resolve ${url.hostname}. Check your internet connection.`));
79
+ } else {
80
+ reject(new CLIError(`Network error: ${err.message}`));
81
+ }
82
+ });
83
+
84
+ if (body) {
85
+ req.write(JSON.stringify(body));
86
+ }
87
+ req.end();
88
+ });
89
+ }
90
+
91
+ class CLIError extends Error {
92
+ constructor(message, statusCode) {
93
+ super(message);
94
+ this.name = 'CLIError';
95
+ this.statusCode = statusCode;
96
+ }
97
+ }
98
+
99
+ function requireAuth() {
100
100
  const token = config.getToken();
101
101
  if (!token) {
102
102
  ui.error('Admin auth is not connected. Run `securenow admin login` first.');
103
103
  process.exit(1);
104
104
  }
105
- return token;
106
- }
107
-
108
- const api = {
109
- get: (endpoint, opts) => request('GET', endpoint, opts),
110
- post: (endpoint, body, opts) => request('POST', endpoint, { body, ...opts }),
111
- put: (endpoint, body, opts) => request('PUT', endpoint, { body, ...opts }),
112
- patch: (endpoint, body, opts) => request('PATCH', endpoint, { body, ...opts }),
113
- delete: (endpoint, opts) => request('DELETE', endpoint, opts),
114
- };
115
-
116
- module.exports = { api, CLIError, requireAuth, buildQueryString };
105
+ return token;
106
+ }
107
+
108
+ const api = {
109
+ get: (endpoint, opts) => request('GET', endpoint, opts),
110
+ post: (endpoint, body, opts) => request('POST', endpoint, { body, ...opts }),
111
+ put: (endpoint, body, opts) => request('PUT', endpoint, { body, ...opts }),
112
+ patch: (endpoint, body, opts) => request('PATCH', endpoint, { body, ...opts }),
113
+ delete: (endpoint, opts) => request('DELETE', endpoint, opts),
114
+ };
115
+
116
+ module.exports = { api, CLIError, requireAuth, buildQueryString };
@@ -555,6 +555,10 @@ async function eventSend(args, flags) {
555
555
  if (Object.keys(attributes).length) ev.attributes = attributes;
556
556
 
557
557
  const headers = { 'Content-Type': 'application/json', ...cfg.headers };
558
+ // `--env` must win over the environment stored with the runtime credentials.
559
+ if ((flags.env || flags.environment) && cfg.deploymentEnvironment) {
560
+ headers['x-securenow-environment'] = cfg.deploymentEnvironment;
561
+ }
558
562
  const spin = ui.spinner(`Sending event to ${eventsEndpoint}`);
559
563
  try {
560
564
  const res = await httpRequest({ endpoint: eventsEndpoint, headers, body: JSON.stringify({ events: [ev] }) });