apigeelint
Version:
Node module and tool to lint a bundle for an Apigee API Proxy or sharedflow.
267 lines (247 loc) • 8.46 kB
JavaScript
/*
Copyright © 2019-2024,2026 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const ruleId = require("../lintUtil.js").getRuleId(),
debug = require("debug")("apigeelint:" + ruleId),
xpath = require("xpath");
const plugin = {
ruleId,
name: "Check for element placement within GenerateJWT",
fatal: false,
severity: 2, // error
nodeType: "Policy",
enabled: true,
};
const allowedChildren = {
DisplayName: [],
Algorithm: [],
Algorithms: ["Key", "Content"],
IgnoreUnresolvedVariables: [],
SecretKey: ["Value", "Id"],
PrivateKey: ["Value", "Id", "Password"],
PublicKey: ["Value", "Certificate", "JWKS", "Id"],
PasswordKey: ["Value", "Id", "PBKDF2Iterations", "SaltLength"],
Subject: [],
Issuer: [],
Audience: [],
ExpiresIn: [],
AdditionalClaims: ["Claim"],
AdditionalHeaders: ["Claim"],
OutputVariable: [],
};
const _addIssue = (policy, message, line, column) => {
const result = {
ruleId: plugin.ruleId,
severity: plugin.severity,
nodeType: plugin.nodeType,
message,
line,
column,
};
// discard duplicates
if (
!line ||
!column ||
!policy.report.messages.find((m) => m.line == line && m.column == column)
) {
policy.addMessage(result);
}
};
const onPolicy = function (policy, cb) {
let foundIssue = false;
if (policy.getType() === "GenerateJWT") {
try {
debug(`policy ${policy.filePath}...`);
const policyRoot = policy.getElement();
debug(`root ${policyRoot}...`);
const allowedTopLevelElements = Object.keys(allowedChildren).filter(
(key) => !key.includes("/"),
);
// check for at least one of Algorithm or Algorithms, not both
const algOrAlgs = xpath.select(
"/GenerateJWT/*[name()='Algorithm' or name()='Algorithms']",
policyRoot,
);
debug(`found ${algOrAlgs.length} algOrAlgs children...`);
if (algOrAlgs.length == 0) {
foundIssue = true;
_addIssue(policy, `You must specify Algorithm or Algorithms.`, 1, 0);
} else if (algOrAlgs.length != 1) {
foundIssue = true;
algOrAlgs
.slice(1)
.forEach((element) =>
_addIssue(
policy,
`Inappropriate <${element.tagName}> element; You must specify exactly one of Algorithm or Algorithms.`,
element.lineNumber,
element.columnNumber,
),
);
}
// check for unknown/unsupported elements at the top level
const foundTopLevelChildren = xpath.select("/GenerateJWT/*", policyRoot);
debug(`found ${foundTopLevelChildren.length} toplevel children...`);
foundTopLevelChildren.forEach((child) => {
debug(`toplevel child: ${child.tagName}...`);
if (!allowedTopLevelElements.includes(child.tagName)) {
foundIssue = true;
_addIssue(
policy,
`element <${child.tagName}> is not allowed here.`,
child.lineNumber,
child.columnNumber,
);
}
});
// For 1st level children, there should be at most one of each
allowedTopLevelElements.forEach((elementName) => {
const elements = xpath.select(`GenerateJWT/${elementName}`, policyRoot);
if (elements.length != 0 && elements.length != 1) {
foundIssue = true;
elements
.slice(1)
.forEach((element) =>
_addIssue(
policy,
`extra <${elementName}> element.`,
element.lineNumber,
element.columnNumber,
),
);
}
});
// check for key agreement with Algorithm
if (algOrAlgs.length > 0) {
debug(`check key and alg agreement...${algOrAlgs[0].tagName}`);
let algText = xpath.select1("text()", algOrAlgs[0]);
algText = algText && algText.data.trim();
debug(`alg ...${algText}`);
// example only the first element
const keyElementNames = Object.keys(allowedChildren).filter((name) =>
name.endsWith("Key"),
);
const keySelector = keyElementNames
.map((n) => `name()='${n}'`)
.join(" or ");
debug(`keySelector: ${keySelector}`);
const keyChildren = xpath.select(
`/GenerateJWT/*[${keySelector}]`,
policyRoot,
);
const flagMissingKey = (requiredKey) => {
foundIssue = true;
_addIssue(
policy,
`The policy must include a <${requiredKey}> when the algorithm is ${algText}.`,
algOrAlgs[0].lineNumber,
algOrAlgs[0].columnNumber,
);
};
const flagWrongKeys = (wrongKeys, expectedTag) => {
foundIssue = true;
wrongKeys.forEach((wrongKey) =>
_addIssue(
policy,
wrongKey.tagName == expectedTag
? `Duplicate <${wrongKey.tagName}> tag.`
: `<${wrongKey.tagName}> is not allowed here; when the algorithm is ${algText}, there must be exactly one Key element, named <${expectedTag}>.`,
wrongKey.lineNumber,
wrongKey.columnNumber,
),
);
};
const checkKeys = (expectedKeyTag) => {
const foundCorrectKey = keyChildren.find(
(child) => child.tagName == expectedKeyTag,
);
if (!foundCorrectKey) {
flagMissingKey(expectedKeyTag);
}
const wrongKeys = keyChildren.filter(
(keyChild, ix) =>
keyChild.tagName !== expectedKeyTag ||
keyChildren.findIndex((k) => k.tagName === expectedKeyTag) !== ix,
);
if (wrongKeys.length > 0) {
flagWrongKeys(wrongKeys, expectedKeyTag);
}
};
// if (keyChildren.length <= 1) {
if (algOrAlgs[0].tagName == "Algorithm") {
if (
// comma-separated list of algs is ok
["HS256", "HS384", "HS512"].find((a) => algText.startsWith(a))
) {
checkKeys("SecretKey");
} else if (
// comma-separated list of algs is ok
[
"RS256",
"RS384",
"RS512",
"PS256",
"PS384",
"PS512",
"ES256",
"ES384",
"ES512",
].find((a) => algText.startsWith(a))
) {
checkKeys("PrivateKey");
} else {
foundIssue = true;
_addIssue(
policy,
`Unrecognized algorithm: ${algText}.`,
algOrAlgs[0].lineNumber,
algOrAlgs[0].columnNumber,
);
}
}
}
// For any valid element, check allowed children.
Object.keys(allowedChildren).forEach((elementName) => {
const elements = xpath.select(`GenerateJWT/${elementName}`, policyRoot);
elements.forEach((element) => {
const qualifiedPath = `${element.parentNode.tagName}/${element.tagName}`;
debug(`checking(1) ${qualifiedPath}...`);
//debug(`${util.format(element)}...`);
const elementChildren = xpath.select(`*`, element);
elementChildren.forEach((child) => {
if (!allowedChildren[element.tagName].includes(child.tagName)) {
foundIssue = true;
_addIssue(
policy,
`element <${child.tagName}> is not allowed here.`,
child.lineNumber,
child.columnNumber,
);
}
});
});
});
// future: add other checks here.
/* c8 ignore start */
} catch (e) {
console.log(e);
}
/* c8 ignore stop */
}
if (typeof cb == "function") {
cb(null, foundIssue);
}
};
module.exports = {
plugin,
onPolicy,
};