@adobe/spacecat-shared-data-access 4.15.0 → 4.15.2

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/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [@adobe/spacecat-shared-data-access-v4.15.2](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v4.15.1...@adobe/spacecat-shared-data-access-v4.15.2) (2026-07-29)
2
+
3
+ ### Bug Fixes
4
+
5
+ * **site:** guard config writes against the config schema ([#1852](https://github.com/adobe/spacecat-shared/issues/1852)) ([dafbea2](https://github.com/adobe/spacecat-shared/commit/dafbea2619d1ff65f27b0fe5ca47734b2622619d)), closes [#1850](https://github.com/adobe/spacecat-shared/issues/1850)
6
+
7
+ ## [@adobe/spacecat-shared-data-access-v4.15.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v4.15.0...@adobe/spacecat-shared-data-access-v4.15.1) (2026-07-29)
8
+
9
+ ### Bug Fixes
10
+
11
+ * validate cdnlogsFilter key against a column allowlist in site config ([#1851](https://github.com/adobe/spacecat-shared/issues/1851)) ([46a0408](https://github.com/adobe/spacecat-shared/commit/46a0408b852fa6f6483fc2ad81c3ce1a8d0c615d)), closes [adobe/spacecat-audit-worker#2826](https://github.com/adobe/spacecat-audit-worker/issues/2826)
12
+
1
13
  ## [@adobe/spacecat-shared-data-access-v4.15.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v4.14.0...@adobe/spacecat-shared-data-access-v4.15.0) (2026-07-29)
2
14
 
3
15
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-data-access",
3
- "version": "4.15.0",
3
+ "version": "4.15.2",
4
4
  "description": "Shared modules of the Spacecat Services - Data Access",
5
5
  "type": "module",
6
6
  "engines": {
@@ -70,6 +70,30 @@ const RESERVED_SCRAPER_HEADER_NAMES = new Set([
70
70
 
71
71
  const LLMO_TAG_PATTERN = /^(market|product|topic):\s?.+/;
72
72
  const AWS_REGION_PATTERN = /^[a-z]{2}(?:-[a-z]+)+-\d+$/i;
73
+
74
+ // Columns on the CDN log tables that a cdnlogsFilter entry is allowed to target.
75
+ // `key` is interpolated (unquoted) into Athena SQL by the audit worker's
76
+ // buildSiteFilters(), so it MUST be constrained to this allowlist to prevent SQL
77
+ // injection (VULN-37491). Keep in sync with ALLOWED_FILTER_KEYS in
78
+ // spacecat-audit-worker/src/utils/cdn-utils.js.
79
+ export const CDN_LOGS_FILTER_KEYS = [
80
+ 'url',
81
+ 'host',
82
+ 'x_forwarded_host',
83
+ 'user_agent',
84
+ 'referer',
85
+ 'cdn_provider',
86
+ ];
87
+
88
+ const CDN_LOGS_FILTER_SCHEMA = Joi.array().items(
89
+ Joi.object({
90
+ // Athena folds column identifiers to lowercase; normalize before matching
91
+ // the allowlist so a stored 'X_forwarded_host' is accepted as the same column.
92
+ key: Joi.string().lowercase().valid(...CDN_LOGS_FILTER_KEYS).required(),
93
+ value: Joi.array().items(Joi.string()).required(),
94
+ type: Joi.string().valid('include', 'exclude').optional(),
95
+ }),
96
+ );
73
97
  const LLMO_TAG = Joi.alternatives()
74
98
  .try(
75
99
  // Tag market, product, topic like this: "market: ch", "product: firefly", "topic: copyright"
@@ -351,13 +375,7 @@ export const configSchema = Joi.object({
351
375
  }),
352
376
  ).optional(),
353
377
  tags: Joi.array().items(Joi.string()).optional(),
354
- cdnlogsFilter: Joi.array().items(
355
- Joi.object({
356
- key: Joi.string().required(),
357
- value: Joi.array().items(Joi.string()).required(),
358
- type: Joi.string().valid('include', 'exclude').optional(),
359
- }),
360
- ).optional(),
378
+ cdnlogsFilter: CDN_LOGS_FILTER_SCHEMA.optional(),
361
379
  countryCodeIgnoreList: Joi.array().items(
362
380
  Joi.string().length(2),
363
381
  ).optional(),
@@ -842,8 +860,17 @@ export const Config = (data = {}) => {
842
860
  };
843
861
 
844
862
  self.updateLlmoCdnlogsFilter = (cdnlogsFilter) => {
863
+ // Reject invalid filter keys at write time (VULN-37491): `key` is
864
+ // interpolated into Athena SQL downstream, so anything off the allowlist
865
+ // must never be persisted. Validate only the filter here to avoid coupling
866
+ // to unrelated required fields elsewhere in the config.
867
+ const { error, value } = CDN_LOGS_FILTER_SCHEMA.validate(cdnlogsFilter);
868
+ if (error) {
869
+ throw new Error(`CDN logs filter validation error: ${error.message}`, { cause: error });
870
+ }
871
+
845
872
  state.llmo = state.llmo || {};
846
- state.llmo.cdnlogsFilter = cdnlogsFilter;
873
+ state.llmo.cdnlogsFilter = value;
847
874
  };
848
875
 
849
876
  self.updateLlmoCountryCodeIgnoreList = (countryCodeIgnoreList) => {
@@ -18,6 +18,8 @@ import {
18
18
  AUTHORING_TYPES,
19
19
  } from '@adobe/spacecat-shared-utils';
20
20
  import BaseModel from '../base/base.model.js';
21
+ import { validateConfiguration } from './config.js';
22
+ import { guardConfigValidation } from '../../util/config-validation-guard.js';
21
23
 
22
24
  const HLX_HOST = /\.(?:aem|hlx)\.(?:page|live)$/i;
23
25
  export const AEM_CS_HOST = /^author-p(\d+)-e(\d+)/i;
@@ -84,6 +86,34 @@ class Site extends BaseModel {
84
86
  return this;
85
87
  }
86
88
 
89
+ /**
90
+ * Sets the site config, guarding it against the config schema
91
+ * (`validateConfiguration`). Overrides the auto-generated setter so every
92
+ * writer (e.g. the `PATCH /sites/{id}` config merge) is checked at one
93
+ * chokepoint. Behavior is governed by `CONFIG_VALIDATION_ENFORCEMENT`
94
+ * (default `warn`; `enforce` throws; `off` skips) — see
95
+ * config-validation-guard.js for the rollout rationale.
96
+ *
97
+ * Note: this only guards writes. Reads still go through the lenient
98
+ * `Config()` getter (attribute `get:` transform), which intentionally
99
+ * tolerates legacy invalid config already stored on existing sites so a
100
+ * bad historical record doesn't break every read of that site.
101
+ *
102
+ * @param {object} value - candidate config object
103
+ * @returns {this}
104
+ */
105
+ setConfig(value) {
106
+ guardConfigValidation({
107
+ entityName: Site.ENTITY_NAME,
108
+ entityId: this.getId(),
109
+ value,
110
+ validate: validateConfiguration,
111
+ log: this.log,
112
+ });
113
+ this.patcher.patchValue('config', value, false);
114
+ return this;
115
+ }
116
+
87
117
  /**
88
118
  * Resolves the site's base URL to a final URL by fetching the URL,
89
119
  * following the redirects and returning the final URL.
@@ -0,0 +1,76 @@
1
+ /*
2
+ * Copyright 2026 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import { ValidationError } from '../errors/index.js';
13
+
14
+ export const ENFORCEMENT_MODES = {
15
+ OFF: 'off',
16
+ WARN: 'warn',
17
+ ENFORCE: 'enforce',
18
+ };
19
+
20
+ /**
21
+ * Resolves the config-validation enforcement mode from the environment.
22
+ *
23
+ * `CONFIG_VALIDATION_ENFORCEMENT` ∈ { off, warn, enforce }; defaults to `warn`.
24
+ * Read at call time so deployments (and tests) can flip it without a re-import.
25
+ *
26
+ * Rollout rationale: `Site.setConfig()` previously accepted any value with no
27
+ * schema validation at all on the update path (only entity `create()` runs
28
+ * attribute validation — see base.collection.js `#validateItem`/`#prepareItem`).
29
+ * Some existing sites carry legacy config that already fails today's Joi
30
+ * schema (e.g. an `imports` entry that predates a stricter schema addition).
31
+ * Flipping straight to `enforce` would reject unrelated PATCH requests on
32
+ * those sites. Ship `warn` first to surface violations in logs, then flip to
33
+ * `enforce` once legacy data is cleaned up or confirmed rare.
34
+ *
35
+ * @returns {string} one of ENFORCEMENT_MODES
36
+ */
37
+ export const getConfigEnforcementMode = () => {
38
+ const raw = (process.env.CONFIG_VALIDATION_ENFORCEMENT || '').trim().toLowerCase();
39
+ return Object.values(ENFORCEMENT_MODES).includes(raw) ? raw : ENFORCEMENT_MODES.WARN;
40
+ };
41
+
42
+ /**
43
+ * Guards a config write. Validates `value` against the config schema via
44
+ * `validateConfiguration`. A valid config passes silently. An invalid config
45
+ * is, depending on the enforcement mode, ignored (`off`), logged without
46
+ * blocking (`warn`), or rejected (`enforce`).
47
+ *
48
+ * @param {object} params
49
+ * @param {string} params.entityName - e.g. 'Site' (for the log message)
50
+ * @param {string} [params.entityId] - entity id (for the log message)
51
+ * @param {object} params.value - the candidate config object
52
+ * @param {(value: object) => object} params.validate - validateConfiguration
53
+ * @param {object} [params.log] - logger with a `warn` method
54
+ * @throws {ValidationError} in `enforce` mode when validation fails
55
+ */
56
+ export const guardConfigValidation = ({
57
+ entityName, entityId, value, validate, log,
58
+ }) => {
59
+ const mode = getConfigEnforcementMode();
60
+ if (mode === ENFORCEMENT_MODES.OFF) {
61
+ return;
62
+ }
63
+
64
+ try {
65
+ validate(value);
66
+ } catch (error) {
67
+ const message = `config validation violation: ${entityName} ${entityId ?? '<unknown>'}: ${error.message}`;
68
+ if (log) {
69
+ log.warn(message);
70
+ }
71
+
72
+ if (mode === ENFORCEMENT_MODES.ENFORCE) {
73
+ throw new ValidationError(`Invalid config for ${entityName}: ${error.message}`);
74
+ }
75
+ }
76
+ };