isc-transforms-mcp 1.0.21 → 1.0.23

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/allowlist.js CHANGED
@@ -28,9 +28,16 @@ const RULES = [
28
28
  { method: "PATCH", pathPrefix: "/v2024/form-instances", modes: ["write"] } // patch-form-instance
29
29
  ];
30
30
  export function isAllowed(mode, method, path) {
31
- return RULES.some(r => r.method === method &&
32
- path.startsWith(r.pathPrefix) &&
33
- r.modes.includes(mode));
31
+ return RULES.some(r => {
32
+ if (r.method !== method || !r.modes.includes(mode))
33
+ return false;
34
+ if (!path.startsWith(r.pathPrefix))
35
+ return false;
36
+ // Ensure prefix is followed by end-of-string, '/', or '?' to prevent
37
+ // matching unintended paths (e.g. /v3/transforms-evil matching /v3/transforms)
38
+ const rest = path.slice(r.pathPrefix.length);
39
+ return rest === "" || rest[0] === "/" || rest[0] === "?";
40
+ });
34
41
  }
35
42
  export function getAllowlist() {
36
43
  return RULES;
package/dist/redact.js CHANGED
@@ -4,7 +4,14 @@ const SECRET_KEYS = new Set([
4
4
  "refresh_token",
5
5
  "client_secret",
6
6
  "secret",
7
- "token"
7
+ "token",
8
+ "password",
9
+ "api_key",
10
+ "apikey",
11
+ "bearer",
12
+ "pat_client_secret",
13
+ "credential",
14
+ "credentials",
8
15
  ]);
9
16
  export function redactDeep(obj) {
10
17
  return redactAny(obj);
@@ -129,7 +129,8 @@ function checkRequired(spec, attrs) {
129
129
  continue;
130
130
  }
131
131
  const val = attrs?.[req];
132
- if (val === undefined || val === null || (typeof val === "string" && val.length === 0)) {
132
+ // Empty string "" is intentional for fields like negativeCondition, positiveCondition, static.value
133
+ if (val === undefined || val === null) {
133
134
  push(msgs, "error", `Missing required attribute: ${req}.`, `attributes.${req}`);
134
135
  }
135
136
  }
@@ -276,25 +277,30 @@ function lintConditional(attrs) {
276
277
  return msgs;
277
278
  }
278
279
  // --- 2. Forbidden operators (using any of these throws IllegalArgumentException at runtime) ---
280
+ // If a forbidden operator is found, that is the root cause. Skip rules 3 and 4 to avoid
281
+ // duplicate errors (they would fire as consequences of the forbidden operator, not separate issues).
279
282
  const forbidden = /(!=|==|>=|<=|>|<|\bne\b|\bgt\b|\blt\b|\bge\b|\ble\b)/i;
280
- if (forbidden.test(expr)) {
281
- push(msgs, "error", `Unsupported operator in expression '${expr}'. ` +
282
- "Only 'eq' is supported using !=, ==, >, <, ne, gt, lt, ge, le throws IllegalArgumentException at runtime.", "attributes.expression");
283
+ const forbiddenOpFound = forbidden.test(expr);
284
+ if (forbiddenOpFound) {
285
+ push(msgs, "error", `Unsupported operator in expression: '${expr}'. Only 'eq' comparator is supported ` +
286
+ "(e.g., '$var eq value'). Using !=, ==, >, <, ne, gt, lt, ge, le throws IllegalArgumentException at runtime.", "attributes.expression");
283
287
  }
284
- // --- 3. Must contain exactly one 'eq' ---
288
+ // --- 3. Must contain exactly one 'eq' (skip if forbidden operator already diagnosed) ---
285
289
  const eqMatches = expr.match(/\beq\b/gi) ?? [];
286
- if (eqMatches.length === 0) {
287
- push(msgs, "error", `Conditional expression must use the 'eq' comparator: '<ValueA> eq <ValueB>'. Got: '${expr}'.`, "attributes.expression");
288
- }
289
- else if (eqMatches.length > 1) {
290
- push(msgs, "error", `Expression must contain exactly one 'eq'. Found ${eqMatches.length} occurrences in: '${expr}'. ` +
291
- "Nest multiple conditions using separate conditional transforms if needed.", "attributes.expression");
290
+ if (!forbiddenOpFound) {
291
+ if (eqMatches.length === 0) {
292
+ push(msgs, "error", `Conditional expression must use the 'eq' comparator: '<ValueA> eq <ValueB>'. Got: '${expr}'.`, "attributes.expression");
293
+ }
294
+ else if (eqMatches.length > 1) {
295
+ push(msgs, "error", `Expression must contain exactly one 'eq'. Found ${eqMatches.length} occurrences in: '${expr}'. ` +
296
+ "Nest multiple conditions using separate conditional transforms if needed.", "attributes.expression");
297
+ }
292
298
  }
293
- // --- 4. Both sides of 'eq' must be non-empty ---
299
+ // --- 4. Both sides of 'eq' must be non-empty (skip if forbidden operator already diagnosed) ---
294
300
  const parts = expr.split(/\beq\b/i);
295
301
  const valueA = parts[0]?.trim() ?? "";
296
302
  const valueB = parts[1]?.trim() ?? "";
297
- if (parts.length !== 2 || valueA.length === 0 || valueB.length === 0) {
303
+ if (!forbiddenOpFound && (parts.length !== 2 || valueA.length === 0 || valueB.length === 0)) {
298
304
  push(msgs, "error", `Expression must follow '<ValueA> eq <ValueB>' with non-empty values on both sides. Got: '${expr}'.`, "attributes.expression");
299
305
  }
300
306
  // --- 5. Case-sensitivity info for literal operands ---
@@ -839,6 +845,13 @@ function lintDateFormat(attrs) {
839
845
  "(expected tokens like y, M, d, H, h, m, s, S, Z). " +
840
846
  `Valid named formats: ${Array.from(NAMED_FORMATS).join(", ")}.`, `attributes.${field}`);
841
847
  }
848
+ // Warn on uppercase Y (week-year) — a very common Java SimpleDateFormat mistake.
849
+ // YYYY = ISO 8601 week-based year (e.g. 2023-12-30 could become 2024); yyyy = calendar year.
850
+ if (/Y/.test(t) && !/^[A-Z][A-Z0-9_]+$/.test(t)) {
851
+ push(msgs, "warn", `${field} '${t}' contains uppercase 'Y' which is the ISO 8601 week-year in Java SimpleDateFormat, NOT the calendar year. ` +
852
+ "This is a common mistake: 'YYYY' for a December date can silently produce the following year's number. " +
853
+ "Use lowercase 'yyyy' for the calendar year (e.g., 'dd-MM-yyyy' instead of 'dd-MM-YYYY').", `attributes.${field}`);
854
+ }
842
855
  };
843
856
  checkFmt("inputFormat");
844
857
  checkFmt("outputFormat");
@@ -1313,9 +1326,20 @@ function lintStatic(attrs) {
1313
1326
  while ((m = refPattern.exec(value)) !== null) {
1314
1327
  vtlRefs.add(m[1]);
1315
1328
  }
1329
+ // 3a. Detect VTL-local variables assigned via #set($var = ...) — these are internal
1330
+ // VTL temporaries, NOT dynamic attributes that need to be defined in the attributes object.
1331
+ const vtlLocalVars = new Set();
1332
+ const setPattern = /#set\(\s*\$([A-Za-z_][A-Za-z0-9_]*)\s*[=\s]/g;
1333
+ let sm;
1334
+ while ((sm = setPattern.exec(value)) !== null) {
1335
+ vtlLocalVars.add(sm[1]);
1336
+ }
1316
1337
  if (vtlRefs.size > 0) {
1317
- // 4. Warn for each VTL reference that has no matching dynamic variable in attributes
1338
+ // 4. Warn for each VTL reference that has no matching dynamic variable in attributes.
1339
+ // Skip VTL-local variables (those assigned with #set inside the template itself).
1318
1340
  for (const varName of vtlRefs) {
1341
+ if (vtlLocalVars.has(varName))
1342
+ continue; // #set local — not an attribute
1319
1343
  if (!Object.prototype.hasOwnProperty.call(attrs, varName)) {
1320
1344
  push(msgs, "warn", `VTL references $${varName} in value but no dynamic variable '${varName}' is defined in attributes. ` +
1321
1345
  `Add a '${varName}' key to attributes with a static string or nested transform that supplies the value.`, `attributes.${varName}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "isc-transforms-mcp",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "type": "module",
5
5
  "description": "MCP server for SailPoint Identity Security Cloud (ISC) Transform authoring — scaffold, strict lint, catalog, and safe upsert to live tenants.",
6
6
  "author": {
@@ -59,12 +59,15 @@
59
59
  "@modelcontextprotocol/sdk": "^1.0.0",
60
60
  "ajv": "^8.18.0",
61
61
  "ajv-formats": "^3.0.1",
62
+ "cors": "^2.8.6",
62
63
  "dotenv": "^16.4.5",
63
64
  "express": "^4.22.1",
65
+ "express-rate-limit": "^8.3.1",
64
66
  "fast-json-patch": "^3.1.1",
65
67
  "zod": "^3.23.8"
66
68
  },
67
69
  "devDependencies": {
70
+ "@types/cors": "^2.8.19",
68
71
  "@types/express": "^4.17.25",
69
72
  "@types/node": "^22.10.0",
70
73
  "tsx": "^4.19.2",