@arcis/node 1.4.3 → 1.4.4

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.
Files changed (45) hide show
  1. package/README.md +11 -3
  2. package/dist/cli/arcis.d.ts +23 -0
  3. package/dist/cli/arcis.d.ts.map +1 -0
  4. package/dist/cli/arcis.js +312 -0
  5. package/dist/cli/arcis.js.map +1 -0
  6. package/dist/cli/arcis.mjs +309 -0
  7. package/dist/cli/arcis.mjs.map +1 -0
  8. package/dist/core/constants.d.ts +1 -1
  9. package/dist/core/constants.d.ts.map +1 -1
  10. package/dist/core/index.js +4 -1
  11. package/dist/core/index.js.map +1 -1
  12. package/dist/core/index.mjs +4 -1
  13. package/dist/core/index.mjs.map +1 -1
  14. package/dist/core/types.d.ts +11 -0
  15. package/dist/core/types.d.ts.map +1 -1
  16. package/dist/index.js +253 -141
  17. package/dist/index.js.map +1 -1
  18. package/dist/index.mjs +253 -141
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/logging/index.js.map +1 -1
  21. package/dist/logging/index.mjs.map +1 -1
  22. package/dist/middleware/bot-detection.d.ts.map +1 -1
  23. package/dist/middleware/csrf.d.ts.map +1 -1
  24. package/dist/middleware/index.js +224 -3
  25. package/dist/middleware/index.js.map +1 -1
  26. package/dist/middleware/index.mjs +224 -3
  27. package/dist/middleware/index.mjs.map +1 -1
  28. package/dist/middleware/main.d.ts.map +1 -1
  29. package/dist/sanitizers/index.d.ts +2 -1
  30. package/dist/sanitizers/index.d.ts.map +1 -1
  31. package/dist/sanitizers/index.js +213 -145
  32. package/dist/sanitizers/index.js.map +1 -1
  33. package/dist/sanitizers/index.mjs +213 -146
  34. package/dist/sanitizers/index.mjs.map +1 -1
  35. package/dist/sanitizers/sanitize.d.ts +13 -0
  36. package/dist/sanitizers/sanitize.d.ts.map +1 -1
  37. package/dist/stores/index.js.map +1 -1
  38. package/dist/stores/index.mjs.map +1 -1
  39. package/dist/telemetry/client.d.ts +3 -0
  40. package/dist/telemetry/client.d.ts.map +1 -1
  41. package/dist/telemetry/types.d.ts +12 -0
  42. package/dist/telemetry/types.d.ts.map +1 -1
  43. package/dist/validation/index.js.map +1 -1
  44. package/dist/validation/index.mjs.map +1 -1
  45. package/package.json +4 -1
package/dist/index.mjs CHANGED
@@ -74,7 +74,10 @@ var XSS_PATTERNS = [
74
74
  /** base href hijacking — redirects all relative URLs to attacker domain */
75
75
  /<base[\s>]/gi,
76
76
  /** link tag injection — stylesheet or preload CSRF attacks */
77
- /<link[\s>]/gi
77
+ /<link[\s>]/gi,
78
+ /** style tag — CSS expression() / behavior: / IE-era attacks. Mirrors
79
+ * Python's xss-style-tag from packages/core/patterns.json. */
80
+ /<style[\s>]/gi
78
81
  ];
79
82
  var XSS_REMOVE_PATTERNS = [
80
83
  /** Full script blocks (content + tags) */
@@ -889,144 +892,6 @@ function detectCommandInjection(input) {
889
892
  return false;
890
893
  }
891
894
 
892
- // src/sanitizers/sanitize.ts
893
- function sanitizeString(value, options = {}) {
894
- if (typeof value !== "string") return value;
895
- const maxSize = options.maxSize ?? INPUT.DEFAULT_MAX_SIZE;
896
- if (value.length > maxSize) {
897
- throw new InputTooLargeError(maxSize, value.length);
898
- }
899
- const reject = options.mode === "reject";
900
- let result = value;
901
- if (options.sql !== false) {
902
- if (reject) {
903
- if (detectSql(result)) {
904
- throw new SecurityThreatError("sql_injection", "SQL pattern detected in input");
905
- }
906
- } else {
907
- result = sanitizeSql(result);
908
- }
909
- }
910
- if (options.path !== false) {
911
- result = sanitizePath(result);
912
- }
913
- if (options.command !== false) {
914
- if (reject) {
915
- if (detectCommandInjection(result)) {
916
- throw new SecurityThreatError("command_injection", "Shell metacharacter detected in input");
917
- }
918
- } else {
919
- result = sanitizeCommand(result);
920
- }
921
- }
922
- if (options.xss !== false) {
923
- result = sanitizeXss(result, false, options.htmlEncode ?? false);
924
- }
925
- return result;
926
- }
927
- function sanitizeObject(obj, options = {}) {
928
- if (obj === null || obj === void 0) return obj;
929
- if (typeof obj === "string") return sanitizeString(obj, options);
930
- if (typeof obj !== "object") return obj;
931
- if (Array.isArray(obj)) return obj.map((item) => sanitizeObject(item, options));
932
- const result = sanitizeObjectDepth(obj, options, 0);
933
- return options.freeze ? Object.freeze(result) : result;
934
- }
935
- function sanitizeObjectDepth(obj, options, depth) {
936
- if (depth >= INPUT.MAX_RECURSION_DEPTH) return obj;
937
- const result = {};
938
- for (const key of Object.keys(obj)) {
939
- if (options.proto !== false && DANGEROUS_PROTO_KEYS.has(key.toLowerCase())) {
940
- continue;
941
- }
942
- if (options.nosql !== false && NOSQL_DANGEROUS_KEYS.has(key)) {
943
- continue;
944
- }
945
- const sanitizedKey = sanitizeString(key, options);
946
- const value = obj[key];
947
- if (value === null || value === void 0) {
948
- result[sanitizedKey] = value;
949
- } else if (typeof value === "string") {
950
- result[sanitizedKey] = sanitizeString(value, options);
951
- } else if (Array.isArray(value)) {
952
- result[sanitizedKey] = value.map((item) => sanitizeObject(item, options));
953
- } else if (typeof value === "object") {
954
- result[sanitizedKey] = sanitizeObjectDepth(value, options, depth + 1);
955
- } else {
956
- result[sanitizedKey] = value;
957
- }
958
- }
959
- return result;
960
- }
961
- function createSanitizer(options = {}) {
962
- return (req, _res, next) => {
963
- try {
964
- if (req.body && typeof req.body === "object") {
965
- req.body = sanitizeObject(req.body, options);
966
- }
967
- if (req.query && typeof req.query === "object") {
968
- const sanitizedQuery = sanitizeObject(req.query, options);
969
- Object.defineProperty(req, "query", { value: sanitizedQuery, writable: true, configurable: true });
970
- }
971
- if (req.params && typeof req.params === "object") {
972
- const sanitizedParams = sanitizeObject(req.params, options);
973
- Object.defineProperty(req, "params", { value: sanitizedParams, writable: true, configurable: true });
974
- }
975
- next();
976
- } catch (err) {
977
- next(err);
978
- }
979
- };
980
- }
981
-
982
- // src/sanitizers/nosql.ts
983
- function isDangerousNoSqlKey(key) {
984
- return NOSQL_DANGEROUS_KEYS.has(key);
985
- }
986
- function detectNoSqlInjection(obj, maxDepth = 10) {
987
- if (maxDepth <= 0) return false;
988
- if (obj === null || typeof obj !== "object") return false;
989
- if (Array.isArray(obj)) {
990
- return obj.some((item) => detectNoSqlInjection(item, maxDepth - 1));
991
- }
992
- for (const key of Object.keys(obj)) {
993
- if (isDangerousNoSqlKey(key)) {
994
- return true;
995
- }
996
- const value = obj[key];
997
- if (typeof value === "object" && value !== null) {
998
- if (detectNoSqlInjection(value, maxDepth - 1)) {
999
- return true;
1000
- }
1001
- }
1002
- }
1003
- return false;
1004
- }
1005
-
1006
- // src/sanitizers/prototype.ts
1007
- function isDangerousProtoKey(key) {
1008
- return DANGEROUS_PROTO_KEYS.has(key.toLowerCase());
1009
- }
1010
- function detectPrototypePollution(obj, maxDepth = 10) {
1011
- if (maxDepth <= 0) return false;
1012
- if (obj === null || typeof obj !== "object") return false;
1013
- if (Array.isArray(obj)) {
1014
- return obj.some((item) => detectPrototypePollution(item, maxDepth - 1));
1015
- }
1016
- for (const key of Object.keys(obj)) {
1017
- if (DANGEROUS_PROTO_KEYS.has(key.toLowerCase())) {
1018
- return true;
1019
- }
1020
- const value = obj[key];
1021
- if (typeof value === "object" && value !== null) {
1022
- if (detectPrototypePollution(value, maxDepth - 1)) {
1023
- return true;
1024
- }
1025
- }
1026
- }
1027
- return false;
1028
- }
1029
-
1030
895
  // src/sanitizers/ssti.ts
1031
896
  var SSTI_DETECT_PATTERNS = [
1032
897
  /** Jinja2 / Twig / Nunjucks: {{ ... }} */
@@ -1189,6 +1054,208 @@ function detectXxe(input) {
1189
1054
  return false;
1190
1055
  }
1191
1056
 
1057
+ // src/sanitizers/sanitize.ts
1058
+ function sanitizeString(value, options = {}) {
1059
+ if (typeof value !== "string") return value;
1060
+ const maxSize = options.maxSize ?? INPUT.DEFAULT_MAX_SIZE;
1061
+ if (value.length > maxSize) {
1062
+ throw new InputTooLargeError(maxSize, value.length);
1063
+ }
1064
+ const reject = options.mode === "reject";
1065
+ let result = value;
1066
+ if (options.sql !== false) {
1067
+ if (reject) {
1068
+ if (detectSql(result)) {
1069
+ throw new SecurityThreatError("sql_injection", "SQL pattern detected in input");
1070
+ }
1071
+ } else {
1072
+ result = sanitizeSql(result);
1073
+ }
1074
+ }
1075
+ if (options.path !== false) {
1076
+ result = sanitizePath(result);
1077
+ }
1078
+ if (options.command !== false) {
1079
+ if (reject) {
1080
+ if (detectCommandInjection(result)) {
1081
+ throw new SecurityThreatError("command_injection", "Shell metacharacter detected in input");
1082
+ }
1083
+ } else {
1084
+ result = sanitizeCommand(result);
1085
+ }
1086
+ }
1087
+ if (options.xss !== false) {
1088
+ result = sanitizeXss(result, false, options.htmlEncode ?? false);
1089
+ }
1090
+ return result;
1091
+ }
1092
+ function sanitizeObject(obj, options = {}) {
1093
+ if (obj === null || obj === void 0) return obj;
1094
+ if (typeof obj === "string") return sanitizeString(obj, options);
1095
+ if (typeof obj !== "object") return obj;
1096
+ if (Array.isArray(obj)) return obj.map((item) => sanitizeObject(item, options));
1097
+ const result = sanitizeObjectDepth(obj, options, 0);
1098
+ return options.freeze ? Object.freeze(result) : result;
1099
+ }
1100
+ function sanitizeObjectDepth(obj, options, depth) {
1101
+ if (depth >= INPUT.MAX_RECURSION_DEPTH) return obj;
1102
+ const result = {};
1103
+ for (const key of Object.keys(obj)) {
1104
+ if (options.proto !== false && DANGEROUS_PROTO_KEYS.has(key.toLowerCase())) {
1105
+ continue;
1106
+ }
1107
+ if (options.nosql !== false && NOSQL_DANGEROUS_KEYS.has(key)) {
1108
+ continue;
1109
+ }
1110
+ const sanitizedKey = sanitizeString(key, options);
1111
+ const value = obj[key];
1112
+ if (value === null || value === void 0) {
1113
+ result[sanitizedKey] = value;
1114
+ } else if (typeof value === "string") {
1115
+ result[sanitizedKey] = sanitizeString(value, options);
1116
+ } else if (Array.isArray(value)) {
1117
+ result[sanitizedKey] = value.map((item) => sanitizeObject(item, options));
1118
+ } else if (typeof value === "object") {
1119
+ result[sanitizedKey] = sanitizeObjectDepth(value, options, depth + 1);
1120
+ } else {
1121
+ result[sanitizedKey] = value;
1122
+ }
1123
+ }
1124
+ return result;
1125
+ }
1126
+ function scanThreats(data, depth = 0) {
1127
+ if (depth > INPUT.MAX_RECURSION_DEPTH) return null;
1128
+ if (data && typeof data === "object" && !Array.isArray(data)) {
1129
+ for (const key of Object.keys(data)) {
1130
+ const lower = key.toLowerCase();
1131
+ if (DANGEROUS_PROTO_KEYS.has(lower)) {
1132
+ return { vector: "prototype", rule: "prototype/match", matchedPattern: key };
1133
+ }
1134
+ if (NOSQL_DANGEROUS_KEYS.has(key)) {
1135
+ return { vector: "nosql", rule: "nosql/match", matchedPattern: key };
1136
+ }
1137
+ const inner = scanThreats(data[key], depth + 1);
1138
+ if (inner) return inner;
1139
+ }
1140
+ return null;
1141
+ }
1142
+ if (Array.isArray(data)) {
1143
+ for (const item of data) {
1144
+ const inner = scanThreats(item, depth + 1);
1145
+ if (inner) return inner;
1146
+ }
1147
+ return null;
1148
+ }
1149
+ if (typeof data !== "string") return null;
1150
+ const sample = data.slice(0, 80);
1151
+ if (detectXss(data)) {
1152
+ return { vector: "xss", rule: "xss/match", matchedPattern: sample };
1153
+ }
1154
+ if (detectSsti(data)) {
1155
+ return { vector: "ssti", rule: "ssti/match", matchedPattern: sample };
1156
+ }
1157
+ if (detectXxe(data)) {
1158
+ return { vector: "xxe", rule: "xxe/match", matchedPattern: sample };
1159
+ }
1160
+ if (detectSql(data)) {
1161
+ return { vector: "sql", rule: "sql/match", matchedPattern: sample };
1162
+ }
1163
+ if (detectPathTraversal(data)) {
1164
+ return { vector: "path", rule: "path/match", matchedPattern: sample };
1165
+ }
1166
+ if (detectCommandInjection(data)) {
1167
+ return { vector: "command", rule: "command/match", matchedPattern: sample };
1168
+ }
1169
+ return null;
1170
+ }
1171
+ function createSanitizer(options = {}) {
1172
+ return (req, res, next) => {
1173
+ try {
1174
+ if (options.block) {
1175
+ const hit = scanThreats(req.body) || scanThreats(req.query) || scanThreats(req.params) || scanThreats(req.path);
1176
+ if (hit) {
1177
+ req.__arcis = {
1178
+ vector: hit.vector,
1179
+ rule: hit.rule,
1180
+ severity: "high",
1181
+ matchedPattern: hit.matchedPattern,
1182
+ reason: `${hit.vector} pattern detected in request`,
1183
+ decision: "deny"
1184
+ };
1185
+ res.status(403).json({
1186
+ error: "Request blocked for security reasons",
1187
+ code: "SECURITY_THREAT",
1188
+ vector: hit.vector
1189
+ });
1190
+ return;
1191
+ }
1192
+ }
1193
+ if (req.body && typeof req.body === "object") {
1194
+ req.body = sanitizeObject(req.body, options);
1195
+ }
1196
+ if (req.query && typeof req.query === "object") {
1197
+ const sanitizedQuery = sanitizeObject(req.query, options);
1198
+ Object.defineProperty(req, "query", { value: sanitizedQuery, writable: true, configurable: true });
1199
+ }
1200
+ if (req.params && typeof req.params === "object") {
1201
+ const sanitizedParams = sanitizeObject(req.params, options);
1202
+ Object.defineProperty(req, "params", { value: sanitizedParams, writable: true, configurable: true });
1203
+ }
1204
+ next();
1205
+ } catch (err) {
1206
+ next(err);
1207
+ }
1208
+ };
1209
+ }
1210
+
1211
+ // src/sanitizers/nosql.ts
1212
+ function isDangerousNoSqlKey(key) {
1213
+ return NOSQL_DANGEROUS_KEYS.has(key);
1214
+ }
1215
+ function detectNoSqlInjection(obj, maxDepth = 10) {
1216
+ if (maxDepth <= 0) return false;
1217
+ if (obj === null || typeof obj !== "object") return false;
1218
+ if (Array.isArray(obj)) {
1219
+ return obj.some((item) => detectNoSqlInjection(item, maxDepth - 1));
1220
+ }
1221
+ for (const key of Object.keys(obj)) {
1222
+ if (isDangerousNoSqlKey(key)) {
1223
+ return true;
1224
+ }
1225
+ const value = obj[key];
1226
+ if (typeof value === "object" && value !== null) {
1227
+ if (detectNoSqlInjection(value, maxDepth - 1)) {
1228
+ return true;
1229
+ }
1230
+ }
1231
+ }
1232
+ return false;
1233
+ }
1234
+
1235
+ // src/sanitizers/prototype.ts
1236
+ function isDangerousProtoKey(key) {
1237
+ return DANGEROUS_PROTO_KEYS.has(key.toLowerCase());
1238
+ }
1239
+ function detectPrototypePollution(obj, maxDepth = 10) {
1240
+ if (maxDepth <= 0) return false;
1241
+ if (obj === null || typeof obj !== "object") return false;
1242
+ if (Array.isArray(obj)) {
1243
+ return obj.some((item) => detectPrototypePollution(item, maxDepth - 1));
1244
+ }
1245
+ for (const key of Object.keys(obj)) {
1246
+ if (DANGEROUS_PROTO_KEYS.has(key.toLowerCase())) {
1247
+ return true;
1248
+ }
1249
+ const value = obj[key];
1250
+ if (typeof value === "object" && value !== null) {
1251
+ if (detectPrototypePollution(value, maxDepth - 1)) {
1252
+ return true;
1253
+ }
1254
+ }
1255
+ }
1256
+ return false;
1257
+ }
1258
+
1192
1259
  // src/sanitizers/jsonp.ts
1193
1260
  var SAFE_CALLBACK_PATTERN = /^[a-zA-Z_$][a-zA-Z0-9_$.]*$/;
1194
1261
  var DANGEROUS_CALLBACK_PATTERNS = [
@@ -2408,11 +2475,16 @@ var MAX_BATCH_SIZE = 500;
2408
2475
  var DEFAULT_FLUSH_INTERVAL_MS = 5e3;
2409
2476
  var MIN_FLUSH_INTERVAL_MS = 500;
2410
2477
  var FLUSH_TIMEOUT_MS = 1e4;
2478
+ var DEFAULT_MAX_QUEUE_SIZE = 1e4;
2411
2479
  var TelemetryClient = class {
2412
2480
  constructor(options) {
2413
2481
  this.queue = [];
2414
2482
  this.flushing = false;
2415
2483
  this.closed = false;
2484
+ // Counts events dropped since the last successful flush. Resets to 0
2485
+ // each flush so onQueueOverflow callbacks see "drops in this window"
2486
+ // rather than a monotonic lifetime counter.
2487
+ this.droppedSinceLastFlush = 0;
2416
2488
  if (!options.endpoint || typeof options.endpoint !== "string") {
2417
2489
  throw new TypeError("TelemetryClient: `endpoint` is required");
2418
2490
  }
@@ -2428,8 +2500,14 @@ var TelemetryClient = class {
2428
2500
  options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
2429
2501
  MIN_FLUSH_INTERVAL_MS
2430
2502
  );
2503
+ this.maxQueueSize = Math.max(
2504
+ options.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE,
2505
+ this.batchSize
2506
+ );
2431
2507
  this.onError = options.onError ?? (() => {
2432
2508
  });
2509
+ this.onQueueOverflow = options.onQueueOverflow ?? (() => {
2510
+ });
2433
2511
  this.startTimer();
2434
2512
  }
2435
2513
  /**
@@ -2439,6 +2517,15 @@ var TelemetryClient = class {
2439
2517
  record(event) {
2440
2518
  if (this.closed) return;
2441
2519
  this.queue.push(event);
2520
+ if (this.queue.length > this.maxQueueSize) {
2521
+ const drop = this.queue.length - this.maxQueueSize;
2522
+ this.queue.splice(0, drop);
2523
+ this.droppedSinceLastFlush += drop;
2524
+ try {
2525
+ this.onQueueOverflow(this.droppedSinceLastFlush);
2526
+ } catch {
2527
+ }
2528
+ }
2442
2529
  if (this.queue.length >= this.batchSize) {
2443
2530
  void this.flush();
2444
2531
  }
@@ -2455,6 +2542,7 @@ var TelemetryClient = class {
2455
2542
  try {
2456
2543
  const batch = this.queue.splice(0, this.batchSize);
2457
2544
  await this.send(batch);
2545
+ this.droppedSinceLastFlush = 0;
2458
2546
  } catch (err) {
2459
2547
  this.safeNotify(err);
2460
2548
  } finally {
@@ -2599,7 +2687,10 @@ function arcis(options = {}) {
2599
2687
  cleanupFns.push(() => rateLimiter.close());
2600
2688
  }
2601
2689
  if (options.sanitize !== false) {
2602
- const sanitizeOpts = typeof options.sanitize === "object" ? options.sanitize : {};
2690
+ const sanitizeOpts = typeof options.sanitize === "object" ? { ...options.sanitize } : {};
2691
+ if (options.block && sanitizeOpts.block === void 0) {
2692
+ sanitizeOpts.block = true;
2693
+ }
2603
2694
  const sanitizer = createSanitizer(sanitizeOpts);
2604
2695
  middlewares.push(telemetryClient ? tapSanitizerThreats(sanitizer) : sanitizer);
2605
2696
  }
@@ -3140,6 +3231,13 @@ function botProtection(options = {}) {
3140
3231
  return next();
3141
3232
  }
3142
3233
  if (denySet.has(result.category)) {
3234
+ req.__arcis = {
3235
+ vector: "bot",
3236
+ rule: `bot/${result.category.toLowerCase()}`,
3237
+ severity: "medium",
3238
+ reason: result.name ? `Bot detected: ${result.name}` : "Bot detected",
3239
+ decision: "deny"
3240
+ };
3143
3241
  if (onDetected) {
3144
3242
  return onDetected(req, res, result);
3145
3243
  }
@@ -3147,6 +3245,13 @@ function botProtection(options = {}) {
3147
3245
  return;
3148
3246
  }
3149
3247
  if (defaultAction === "deny") {
3248
+ req.__arcis = {
3249
+ vector: "bot",
3250
+ rule: "bot/uncategorized",
3251
+ severity: "medium",
3252
+ reason: "Uncategorized bot under defaultAction=deny",
3253
+ decision: "deny"
3254
+ };
3150
3255
  if (onDetected) {
3151
3256
  return onDetected(req, res, result);
3152
3257
  }
@@ -3200,7 +3305,14 @@ function csrfProtection(options = {}) {
3200
3305
  sameSite: options.cookie?.sameSite ?? "Lax",
3201
3306
  domain: options.cookie?.domain
3202
3307
  };
3203
- const defaultOnError = (_req, res, _next) => {
3308
+ const defaultOnError = (req, res, _next) => {
3309
+ req.__arcis = {
3310
+ vector: "csrf",
3311
+ rule: "csrf/token-mismatch",
3312
+ severity: "high",
3313
+ reason: "CSRF token missing or invalid",
3314
+ decision: "deny"
3315
+ };
3204
3316
  res.status(403).json({
3205
3317
  error: "CSRF token validation failed",
3206
3318
  message: "Invalid or missing CSRF token. Include the token from the cookie in the X-CSRF-Token header."