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 +10 -3
- package/dist/redact.js +8 -1
- package/dist/transforms/lint.js +38 -14
- package/package.json +4 -1
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 =>
|
|
32
|
-
|
|
33
|
-
|
|
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);
|
package/dist/transforms/lint.js
CHANGED
|
@@ -129,7 +129,8 @@ function checkRequired(spec, attrs) {
|
|
|
129
129
|
continue;
|
|
130
130
|
}
|
|
131
131
|
const val = attrs?.[req];
|
|
132
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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 (
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
"
|
|
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.
|
|
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",
|