utils/lib/ecs-logging-validate.js (143 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you 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 // // http://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. 'use strict' // A package to validate ECS log records against the ecs-logging.git spec. const assert = require('assert') const fs = require('fs') const path = require('path') const { hasOwnProperty } = Object.prototype const SPEC_PATH = path.resolve(__dirname, '..', 'ecs-logging', 'spec.json') class EcsLoggingValidationError extends Error { constructor (details) { assert(details.length, 'non-empty details array is provided') const message = details.map(d => d.message).join(', ') super(message) this.details = details } } function loadSpec () { return JSON.parse(fs.readFileSync(SPEC_PATH)) } // Lookup the property `name` (given in dot-notation) in the object `obj`. // Name "foo.bar" will return 42 from both: // {"foo.bar": 42} // {"foo": {"bar": 42}} // If the named property is not found, this returns `undefined`. function dottedLookup (obj, name) { if (hasOwnProperty.call(obj, name)) { return obj[name] } let o = obj const parts = name.split('.') for (let i = 0; i < parts.length; i++) { const part = parts[i] if (!hasOwnProperty.call(o, part)) { return undefined } o = o[part] } return o } // Validate an ecs-logging record. // // This returns null if the record is valid. If invalid, it returns one of: // 1. a JSON parse error, if an invalid JSON string record is given; or // 2. an instance of `EcsLoggingValidationError`. This Error object has a // `.details` array with details on each validation issue. Each detail // object is of the form: // // { // "message": "...", // "specKey": "<the key in the spec object that failed>", // // The following only if the specKey is not 'index'. // "name": "<the field name>", // "spec": {/* the spec entry for this field */} // } // // for example: // // { // "message": "required field '@timestamp' is missing", // "specKey": "required", // "name": "@timestamp", // "spec": { // "type": "datetime", // "required": true, // "index": 0, // "url": "https://www.elastic.co/guide/en/ecs/current/ecs-base.html" // } // } // // The given record can be either a JSON string, or a parsed object. However, // note that if a parsed object is given, then the "index" spec keys (which // attempt to specify the order of keys in the log record) cannot be validated // because, in general, Node's JSON parsers -- including `JSON.parse` -- do // NOT maintain key order: // > Object.keys(JSON.parse('{"a":true,"1":true,"c":true,"b":true}')) // [ '1', 'a', 'c', 'b' ] // // - @param {String|Object} rec - The log record to validate. If an *object* // is given then validation of "index" cannot be done. // - @param {Object} opts - Optional. Options to control validation. // - {Boolean} opts.ignoreIndex - Set true to ignore "index" in validation. function ecsLoggingValidate (rec, opts) { opts = opts || {} const ignoreIndex = !!opts.ignoreIndex let recObj let recStr = null if (typeof (rec) === 'string') { recStr = rec try { recObj = JSON.parse(recStr) } catch (parseErr) { return parseErr } } else { recObj = rec } const details = [] const addDetail = detail => { if (ignoreIndex && detail.specKey === 'index') { return } details.push(detail) } const spec = loadSpec() const indexedNames = [] // to handle `field.index` for (const [name, field] of Object.entries(spec.fields)) { const specKeysToHandle = Object.assign({}, field) delete specKeysToHandle.comment delete specKeysToHandle.url delete specKeysToHandle.default const recVal = dottedLookup(recObj, name) // field.required if (recVal === undefined) { if (field.required) { addDetail({ message: `required field '${name}' is missing`, specKey: 'required', name: name, spec: field }) } continue } delete specKeysToHandle.required // field.top_level_field if (field.top_level_field && !hasOwnProperty.call(recObj, name)) { addDetail({ message: `field '${name}' is not a top-level field`, specKey: 'top_level_field', name: name, spec: field }) } delete specKeysToHandle.top_level_field // field.index if (field.index !== undefined) { indexedNames[field.index] = name } delete specKeysToHandle.index // field.type switch (field.type) { case 'datetime': // We'll use the approximation that if JavaScript's `new Date()` can // handle it, that it roughly satisfies: // https://www.elastic.co/guide/en/elasticsearch/reference/current/date.html if (new Date(recVal).toString() === 'Invalid Date') { addDetail({ message: `field '${name}' is not a valid '${field.type}'`, specKey: 'type', name: name, spec: field }) } break case 'string': if (typeof (recVal) !== 'string') { addDetail({ message: `field '${name}' is not a valid '${field.type}'`, specKey: 'type', name: name, spec: field }) } break default: throw new Error(`unknown field type: ${field.type}`) } delete specKeysToHandle.type if (Object.keys(specKeysToHandle).length !== 0) { throw new Error('do not know how to handle these ecs-logging spec ' + `fields from field '${name}': ${Object.keys(specKeysToHandle).join(', ')}`) } } // field.index if (indexedNames.length > 0 && recStr) { let expected = ['{'] indexedNames.forEach(n => { expected.push('"') expected.push(n) expected.push('":') expected.push(JSON.stringify(dottedLookup(recObj, n))) expected.push(',') }) expected = expected.join('') if (!recStr.startsWith(expected)) { addDetail({ message: `the order of fields is not the expected: ${indexedNames.join(', ')}`, specKey: 'index' }) } } if (details.length === 0) { return null } else { return new EcsLoggingValidationError(details) } } module.exports = { ecsLoggingValidate }