91 lines
4.5 KiB
JavaScript
91 lines
4.5 KiB
JavaScript
import { TSESTree } from '@typescript-eslint/types';
|
|
import { isNodeTypeClass, findClassProperty, findArrayLiteralProperty, extractCredentialNameFromArray, findPackageJson, readPackageJsonCredentials, isFileType, findSimilarStrings, createRule, } from '../utils/index.js';
|
|
export const NoCredentialReuseRule = createRule({
|
|
name: 'no-credential-reuse',
|
|
meta: {
|
|
type: 'problem',
|
|
docs: {
|
|
description: 'Prevent credential re-use security issues by ensuring nodes only reference credentials from the same package',
|
|
},
|
|
messages: {
|
|
didYouMean: "Did you mean '{{ suggestedName }}'?",
|
|
useAvailable: "Use available credential '{{ suggestedName }}'",
|
|
credentialNotInPackage: 'SECURITY: Node references credential "{{ credentialName }}" which is not defined in this package. This creates a security risk as it attempts to reuse credentials from other packages. Nodes can only use credentials from the same package as listed in package.json n8n.credentials field.',
|
|
},
|
|
schema: [],
|
|
hasSuggestions: true,
|
|
},
|
|
defaultOptions: [],
|
|
create(context) {
|
|
if (!isFileType(context.filename, '.node.ts')) {
|
|
return {};
|
|
}
|
|
let packageCredentials = null;
|
|
const loadPackageCredentials = () => {
|
|
if (packageCredentials !== null) {
|
|
return packageCredentials;
|
|
}
|
|
const packageJsonPath = findPackageJson(context.filename);
|
|
if (!packageJsonPath) {
|
|
packageCredentials = new Set();
|
|
return packageCredentials;
|
|
}
|
|
packageCredentials = readPackageJsonCredentials(packageJsonPath);
|
|
return packageCredentials;
|
|
};
|
|
return {
|
|
ClassDeclaration(node) {
|
|
if (!isNodeTypeClass(node)) {
|
|
return;
|
|
}
|
|
const descriptionProperty = findClassProperty(node, 'description');
|
|
if (!descriptionProperty?.value ||
|
|
descriptionProperty.value.type !== TSESTree.AST_NODE_TYPES.ObjectExpression) {
|
|
return;
|
|
}
|
|
const credentialsArray = findArrayLiteralProperty(descriptionProperty.value, 'credentials');
|
|
if (!credentialsArray) {
|
|
return;
|
|
}
|
|
const allowedCredentials = loadPackageCredentials();
|
|
credentialsArray.elements.forEach((element) => {
|
|
const credentialInfo = extractCredentialNameFromArray(element);
|
|
if (credentialInfo && !allowedCredentials.has(credentialInfo.name)) {
|
|
const similarCredentials = findSimilarStrings(credentialInfo.name, allowedCredentials);
|
|
const suggestions = [];
|
|
for (const similarName of similarCredentials) {
|
|
suggestions.push({
|
|
messageId: 'didYouMean',
|
|
data: { suggestedName: similarName },
|
|
fix(fixer) {
|
|
return fixer.replaceText(credentialInfo.node, `"${similarName}"`);
|
|
},
|
|
});
|
|
}
|
|
if (suggestions.length === 0 && allowedCredentials.size > 0) {
|
|
const availableCredentials = Array.from(allowedCredentials).slice(0, 3);
|
|
for (const availableName of availableCredentials) {
|
|
suggestions.push({
|
|
messageId: 'useAvailable',
|
|
data: { suggestedName: availableName },
|
|
fix(fixer) {
|
|
return fixer.replaceText(credentialInfo.node, `"${availableName}"`);
|
|
},
|
|
});
|
|
}
|
|
}
|
|
context.report({
|
|
node: credentialInfo.node,
|
|
messageId: 'credentialNotInPackage',
|
|
data: {
|
|
credentialName: credentialInfo.name,
|
|
},
|
|
suggest: suggestions,
|
|
});
|
|
}
|
|
});
|
|
},
|
|
};
|
|
},
|
|
});
|
|
//# sourceMappingURL=no-credential-reuse.js.map
|