mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 03:01:21 -04:00
ESLint Rule to discourage hashes being created with unsafe algorithms (#190973)
Closes https://github.com/elastic/kibana/issues/185601 ## Summary Using non-compliant algorithms with Node Cryptos createHash function will cause failures when running Kibana in FIPS mode. We want to discourage usages of such algorithms. --------- Co-authored-by: Sid <siddharthmantri1@gmail.com> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
7aa64b6ed5
commit
f207c2c176
13 changed files with 320 additions and 10 deletions
|
@ -13,7 +13,7 @@ import * as Rx from 'rxjs';
|
||||||
import { map, takeUntil } from 'rxjs';
|
import { map, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
export const generateFileHash = (fd: number): Promise<string> => {
|
export const generateFileHash = (fd: number): Promise<string> => {
|
||||||
const hash = createHash('sha1');
|
const hash = createHash('sha1'); // eslint-disable-line @kbn/eslint/no_unsafe_hash
|
||||||
const read = createReadStream(null as any, {
|
const read = createReadStream(null as any, {
|
||||||
fd,
|
fd,
|
||||||
start: 0,
|
start: 0,
|
||||||
|
|
|
@ -114,7 +114,7 @@ export const bootstrapRendererFactory: BootstrapRendererFactory = ({
|
||||||
publicPathMap,
|
publicPathMap,
|
||||||
});
|
});
|
||||||
|
|
||||||
const hash = createHash('sha1');
|
const hash = createHash('sha1'); // eslint-disable-line @kbn/eslint/no_unsafe_hash
|
||||||
hash.update(body);
|
hash.update(body);
|
||||||
const etag = hash.digest('hex');
|
const etag = hash.digest('hex');
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ type SavedObjectTypeMigrationHash = string;
|
||||||
export const getMigrationHash = (soType: SavedObjectsType): SavedObjectTypeMigrationHash => {
|
export const getMigrationHash = (soType: SavedObjectsType): SavedObjectTypeMigrationHash => {
|
||||||
const migInfo = extractMigrationInfo(soType);
|
const migInfo = extractMigrationInfo(soType);
|
||||||
|
|
||||||
const hash = createHash('sha1');
|
const hash = createHash('sha1'); // eslint-disable-line @kbn/eslint/no_unsafe_hash
|
||||||
|
|
||||||
const hashParts = [
|
const hashParts = [
|
||||||
migInfo.name,
|
migInfo.name,
|
||||||
|
|
|
@ -84,7 +84,7 @@ async function sourceInfo(cwd: string, license: string, log: ToolingLog = defaul
|
||||||
log.info('on %s at %s', chalk.bold(branch), chalk.bold(sha));
|
log.info('on %s at %s', chalk.bold(branch), chalk.bold(sha));
|
||||||
log.info('%s locally modified file(s)', chalk.bold(status.modified.length));
|
log.info('%s locally modified file(s)', chalk.bold(status.modified.length));
|
||||||
|
|
||||||
const etag = crypto.createHash('md5').update(branch);
|
const etag = crypto.createHash('md5').update(branch); // eslint-disable-line @kbn/eslint/no_unsafe_hash
|
||||||
etag.update(sha);
|
etag.update(sha);
|
||||||
|
|
||||||
// for changed files, use last modified times in hash calculation
|
// for changed files, use last modified times in hash calculation
|
||||||
|
@ -92,7 +92,7 @@ async function sourceInfo(cwd: string, license: string, log: ToolingLog = defaul
|
||||||
etag.update(fs.statSync(path.join(cwd, file.path)).mtime.toString());
|
etag.update(fs.statSync(path.join(cwd, file.path)).mtime.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
const cwdHash = crypto.createHash('md5').update(cwd).digest('hex').substr(0, 8);
|
const cwdHash = crypto.createHash('md5').update(cwd).digest('hex').substr(0, 8); // eslint-disable-line @kbn/eslint/no_unsafe_hash
|
||||||
|
|
||||||
const basename = `${branch}-${task}-${cwdHash}`;
|
const basename = `${branch}-${task}-${cwdHash}`;
|
||||||
const filename = `${basename}.${ext}`;
|
const filename = `${basename}.${ext}`;
|
||||||
|
|
|
@ -314,6 +314,7 @@ module.exports = {
|
||||||
'@kbn/eslint/no_constructor_args_in_property_initializers': 'error',
|
'@kbn/eslint/no_constructor_args_in_property_initializers': 'error',
|
||||||
'@kbn/eslint/no_this_in_property_initializers': 'error',
|
'@kbn/eslint/no_this_in_property_initializers': 'error',
|
||||||
'@kbn/eslint/no_unsafe_console': 'error',
|
'@kbn/eslint/no_unsafe_console': 'error',
|
||||||
|
'@kbn/eslint/no_unsafe_hash': 'error',
|
||||||
'@kbn/imports/no_unresolvable_imports': 'error',
|
'@kbn/imports/no_unresolvable_imports': 'error',
|
||||||
'@kbn/imports/uniform_imports': 'error',
|
'@kbn/imports/uniform_imports': 'error',
|
||||||
'@kbn/imports/no_unused_imports': 'error',
|
'@kbn/imports/no_unused_imports': 'error',
|
||||||
|
|
|
@ -19,5 +19,6 @@ module.exports = {
|
||||||
no_constructor_args_in_property_initializers: require('./rules/no_constructor_args_in_property_initializers'),
|
no_constructor_args_in_property_initializers: require('./rules/no_constructor_args_in_property_initializers'),
|
||||||
no_this_in_property_initializers: require('./rules/no_this_in_property_initializers'),
|
no_this_in_property_initializers: require('./rules/no_this_in_property_initializers'),
|
||||||
no_unsafe_console: require('./rules/no_unsafe_console'),
|
no_unsafe_console: require('./rules/no_unsafe_console'),
|
||||||
|
no_unsafe_hash: require('./rules/no_unsafe_hash'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
166
packages/kbn-eslint-plugin-eslint/rules/no_unsafe_hash.js
Normal file
166
packages/kbn-eslint-plugin-eslint/rules/no_unsafe_hash.js
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the "Elastic License
|
||||||
|
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||||
|
* Public License v 1"; you may not use this file except in compliance with, at
|
||||||
|
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||||
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
|
*/
|
||||||
|
|
||||||
|
const allowedAlgorithms = ['sha256', 'sha3-256', 'sha512'];
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
allowedAlgorithms,
|
||||||
|
meta: {
|
||||||
|
type: 'problem',
|
||||||
|
docs: {
|
||||||
|
description: 'Allow usage of createHash only with allowed algorithms.',
|
||||||
|
category: 'FIPS',
|
||||||
|
recommended: false,
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
noDisallowedHash:
|
||||||
|
'Usage of {{functionName}} with "{{algorithm}}" is not allowed. Only the following algorithms are allowed: [{{allowedAlgorithms}}]. If you need to use a different algorithm, please contact the Kibana security team.',
|
||||||
|
},
|
||||||
|
schema: [],
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
let isCreateHashImported = false;
|
||||||
|
let createHashName = 'createHash';
|
||||||
|
let cryptoLocalName = 'crypto';
|
||||||
|
let usedFunctionName = '';
|
||||||
|
const sourceCode = context.getSourceCode();
|
||||||
|
|
||||||
|
const disallowedAlgorithmNodes = new Set();
|
||||||
|
|
||||||
|
function isAllowedAlgorithm(algorithm) {
|
||||||
|
return allowedAlgorithms.includes(algorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHashOrCreateHash(value) {
|
||||||
|
if (value === 'hash' || value === 'createHash') {
|
||||||
|
usedFunctionName = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIdentifierValue(node) {
|
||||||
|
const scope = sourceCode.getScope(node);
|
||||||
|
if (!scope) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const variable = scope.variables.find((variable) => variable.name === node.name);
|
||||||
|
if (variable && variable.defs.length > 0) {
|
||||||
|
const def = variable.defs[0];
|
||||||
|
if (
|
||||||
|
def.node.init &&
|
||||||
|
def.node.init.type === 'Literal' &&
|
||||||
|
!isAllowedAlgorithm(def.node.init.value)
|
||||||
|
) {
|
||||||
|
disallowedAlgorithmNodes.add(node.name);
|
||||||
|
return def.node.init.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ImportDeclaration(node) {
|
||||||
|
if (node.source.value === 'crypto' || node.source.value === 'node:crypto') {
|
||||||
|
node.specifiers.forEach((specifier) => {
|
||||||
|
if (
|
||||||
|
specifier.type === 'ImportSpecifier' &&
|
||||||
|
isHashOrCreateHash(specifier.imported.name)
|
||||||
|
) {
|
||||||
|
isCreateHashImported = true;
|
||||||
|
createHashName = specifier.local.name; // Capture local name (renamed or not)
|
||||||
|
} else if (specifier.type === 'ImportDefaultSpecifier') {
|
||||||
|
cryptoLocalName = specifier.local.name;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
VariableDeclarator(node) {
|
||||||
|
if (node.init && node.init.type === 'Literal' && !isAllowedAlgorithm(node.init.value)) {
|
||||||
|
disallowedAlgorithmNodes.add(node.id.name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
AssignmentExpression(node) {
|
||||||
|
if (
|
||||||
|
node.right.type === 'Literal' &&
|
||||||
|
node.right.value === 'md5' &&
|
||||||
|
node.left.type === 'Identifier'
|
||||||
|
) {
|
||||||
|
disallowedAlgorithmNodes.add(node.left.name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
CallExpression(node) {
|
||||||
|
const callee = node.callee;
|
||||||
|
|
||||||
|
if (
|
||||||
|
callee.type === 'MemberExpression' &&
|
||||||
|
callee.object.name === cryptoLocalName &&
|
||||||
|
isHashOrCreateHash(callee.property.name)
|
||||||
|
) {
|
||||||
|
const arg = node.arguments[0];
|
||||||
|
if (arg) {
|
||||||
|
if (arg.type === 'Literal' && !isAllowedAlgorithm(arg.value)) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'noDisallowedHash',
|
||||||
|
data: {
|
||||||
|
algorithm: arg.value,
|
||||||
|
allowedAlgorithms: allowedAlgorithms.join(', '),
|
||||||
|
functionName: usedFunctionName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (arg.type === 'Identifier') {
|
||||||
|
const identifierValue = getIdentifierValue(arg);
|
||||||
|
if (disallowedAlgorithmNodes.has(arg.name) && identifierValue) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'noDisallowedHash',
|
||||||
|
data: {
|
||||||
|
algorithm: identifierValue,
|
||||||
|
allowedAlgorithms: allowedAlgorithms.join(', '),
|
||||||
|
functionName: usedFunctionName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCreateHashImported && callee.name === createHashName) {
|
||||||
|
const arg = node.arguments[0];
|
||||||
|
if (arg) {
|
||||||
|
if (arg.type === 'Literal' && !isAllowedAlgorithm(arg.value)) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'noDisallowedHash',
|
||||||
|
data: {
|
||||||
|
algorithm: arg.value,
|
||||||
|
allowedAlgorithms: allowedAlgorithms.join(', '),
|
||||||
|
functionName: usedFunctionName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (arg.type === 'Identifier') {
|
||||||
|
const identifierValue = getIdentifierValue(arg);
|
||||||
|
if (disallowedAlgorithmNodes.has(arg.name) && identifierValue) {
|
||||||
|
context.report({
|
||||||
|
node,
|
||||||
|
messageId: 'noDisallowedHash',
|
||||||
|
data: {
|
||||||
|
algorithm: identifierValue,
|
||||||
|
allowedAlgorithms: allowedAlgorithms.join(', '),
|
||||||
|
functionName: usedFunctionName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
142
packages/kbn-eslint-plugin-eslint/rules/no_unsafe_hash.test.js
Normal file
142
packages/kbn-eslint-plugin-eslint/rules/no_unsafe_hash.test.js
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the "Elastic License
|
||||||
|
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||||
|
* Public License v 1"; you may not use this file except in compliance with, at
|
||||||
|
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||||
|
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { RuleTester } = require('eslint');
|
||||||
|
const { allowedAlgorithms, ...rule } = require('./no_unsafe_hash');
|
||||||
|
|
||||||
|
const dedent = require('dedent');
|
||||||
|
|
||||||
|
const joinedAllowedAlgorithms = `[${allowedAlgorithms.join(', ')}]`;
|
||||||
|
|
||||||
|
const ruleTester = new RuleTester({
|
||||||
|
parser: require.resolve('@typescript-eslint/parser'),
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 2018,
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
ruleTester.run('@kbn/eslint/no_unsafe_hash', rule, {
|
||||||
|
valid: [
|
||||||
|
// valid import of crypto and call of createHash
|
||||||
|
{
|
||||||
|
code: dedent`
|
||||||
|
import crypto from 'crypto';
|
||||||
|
crypto.createHash('sha256');
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
// valid import and call of createHash
|
||||||
|
{
|
||||||
|
code: dedent`
|
||||||
|
import { createHash } from 'crypto';
|
||||||
|
createHash('sha256');
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
// valid import and call of createHash with a variable containing a compliant aglorithm
|
||||||
|
{
|
||||||
|
code: dedent`
|
||||||
|
import { createHash } from 'crypto';
|
||||||
|
const myHash = 'sha256';
|
||||||
|
createHash(myHash);
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
// valid import and call of hash with a variable containing a compliant aglorithm
|
||||||
|
{
|
||||||
|
code: dedent`
|
||||||
|
import { hash } from 'crypto';
|
||||||
|
const myHash = 'sha256';
|
||||||
|
hash(myHash);
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
invalid: [
|
||||||
|
// invalid call of createHash when calling from crypto
|
||||||
|
{
|
||||||
|
code: dedent`
|
||||||
|
import crypto from 'crypto';
|
||||||
|
crypto.createHash('md5');
|
||||||
|
`,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
line: 2,
|
||||||
|
message: `Usage of createHash with "md5" is not allowed. Only the following algorithms are allowed: ${joinedAllowedAlgorithms}. If you need to use a different algorithm, please contact the Kibana security team.`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// invalid call of createHash when importing directly
|
||||||
|
{
|
||||||
|
code: dedent`
|
||||||
|
import { createHash } from 'crypto';
|
||||||
|
createHash('md5');
|
||||||
|
`,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
line: 2,
|
||||||
|
message: `Usage of createHash with "md5" is not allowed. Only the following algorithms are allowed: ${joinedAllowedAlgorithms}. If you need to use a different algorithm, please contact the Kibana security team.`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// invalid call of createHash when calling with a variable containing md5
|
||||||
|
{
|
||||||
|
code: dedent`
|
||||||
|
import { createHash } from 'crypto';
|
||||||
|
const myHash = 'md5';
|
||||||
|
createHash(myHash);
|
||||||
|
`,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
line: 3,
|
||||||
|
message: `Usage of createHash with "md5" is not allowed. Only the following algorithms are allowed: ${joinedAllowedAlgorithms}. If you need to use a different algorithm, please contact the Kibana security team.`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// invalid import and call of hash when importing directly
|
||||||
|
{
|
||||||
|
code: dedent`
|
||||||
|
import { hash } from 'crypto';
|
||||||
|
hash('md5');
|
||||||
|
`,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
line: 2,
|
||||||
|
message: `Usage of hash with "md5" is not allowed. Only the following algorithms are allowed: ${joinedAllowedAlgorithms}. If you need to use a different algorithm, please contact the Kibana security team.`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: dedent`
|
||||||
|
import _crypto from 'crypto';
|
||||||
|
_crypto.hash('md5');
|
||||||
|
`,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
line: 2,
|
||||||
|
message: `Usage of hash with "md5" is not allowed. Only the following algorithms are allowed: ${joinedAllowedAlgorithms}. If you need to use a different algorithm, please contact the Kibana security team.`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
code: dedent`
|
||||||
|
import { hash as _hash } from 'crypto';
|
||||||
|
_hash('md5');
|
||||||
|
`,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
line: 2,
|
||||||
|
message: `Usage of hash with "md5" is not allowed. Only the following algorithms are allowed: ${joinedAllowedAlgorithms}. If you need to use a different algorithm, please contact the Kibana security team.`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
|
@ -127,7 +127,7 @@ export async function reportFailuresToFile(
|
||||||
// Jest could, in theory, fail 1000s of tests and write 1000s of failures
|
// Jest could, in theory, fail 1000s of tests and write 1000s of failures
|
||||||
// So let's just write files for the first 20
|
// So let's just write files for the first 20
|
||||||
for (const failure of failures.slice(0, 20)) {
|
for (const failure of failures.slice(0, 20)) {
|
||||||
const hash = createHash('md5').update(failure.name).digest('hex');
|
const hash = createHash('md5').update(failure.name).digest('hex'); // eslint-disable-line @kbn/eslint/no_unsafe_hash
|
||||||
const filenameBase = `${
|
const filenameBase = `${
|
||||||
process.env.BUILDKITE_JOB_ID ? process.env.BUILDKITE_JOB_ID + '_' : ''
|
process.env.BUILDKITE_JOB_ID ? process.env.BUILDKITE_JOB_ID + '_' : ''
|
||||||
}${hash}`;
|
}${hash}`;
|
||||||
|
|
|
@ -20,7 +20,7 @@ export interface ParsedDllManifest {
|
||||||
}
|
}
|
||||||
|
|
||||||
const hash = (s: string) => {
|
const hash = (s: string) => {
|
||||||
return Crypto.createHash('sha1').update(s).digest('base64').replace(/=+$/, '');
|
return Crypto.createHash('sha1').update(s).digest('base64').replace(/=+$/, ''); // eslint-disable-line @kbn/eslint/no_unsafe_hash
|
||||||
};
|
};
|
||||||
|
|
||||||
export function parseDllManifest(manifest: DllManifest): ParsedDllManifest {
|
export function parseDllManifest(manifest: DllManifest): ParsedDllManifest {
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { parseFields, IBody, IQuery, querySchema, validate } from './fields_for'
|
||||||
import { DEFAULT_FIELD_CACHE_FRESHNESS } from '../../constants';
|
import { DEFAULT_FIELD_CACHE_FRESHNESS } from '../../constants';
|
||||||
|
|
||||||
export function calculateHash(srcBuffer: Buffer) {
|
export function calculateHash(srcBuffer: Buffer) {
|
||||||
const hash = createHash('sha1');
|
const hash = createHash('sha1'); // eslint-disable-line @kbn/eslint/no_unsafe_hash
|
||||||
hash.update(srcBuffer);
|
hash.update(srcBuffer);
|
||||||
return hash.digest('hex');
|
return hash.digest('hex');
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ export const renderFullStoryLibraryFactory = (dist = true) =>
|
||||||
headers: HttpResponseOptions['headers'];
|
headers: HttpResponseOptions['headers'];
|
||||||
}> => {
|
}> => {
|
||||||
const srcBuffer = await fs.readFile(FULLSTORY_LIBRARY_PATH);
|
const srcBuffer = await fs.readFile(FULLSTORY_LIBRARY_PATH);
|
||||||
const hash = createHash('sha1');
|
const hash = createHash('sha1'); // eslint-disable-line @kbn/eslint/no_unsafe_hash
|
||||||
hash.update(srcBuffer);
|
hash.update(srcBuffer);
|
||||||
const hashDigest = hash.digest('hex');
|
const hashDigest = hash.digest('hex');
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { CASES_TELEMETRY_TASK_NAME } from '@kbn/cases-plugin/common/constants';
|
||||||
import type { FixtureStartDeps } from './plugin';
|
import type { FixtureStartDeps } from './plugin';
|
||||||
|
|
||||||
const hashParts = (parts: string[]): string => {
|
const hashParts = (parts: string[]): string => {
|
||||||
const hash = createHash('sha1');
|
const hash = createHash('sha1'); // eslint-disable-line @kbn/eslint/no_unsafe_hash
|
||||||
const hashFeed = parts.join('-');
|
const hashFeed = parts.join('-');
|
||||||
return hash.update(hashFeed).digest('hex');
|
return hash.update(hashFeed).digest('hex');
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue