hapi-audit-rest

    3.5.0 • Public • Published

    hapi-audit-rest

    npm version Build Status Known Vulnerabilities Coverage Status

    Small opinionated Hapi.js plugin that generates audit logs for RESTful APIs.

    Contents

    Requirements

    Works with Hapi v18 or higher, Node.js v12 or higher

    Installation

    npm i -S hapi-audit-rest

    Testing

    npm test

    About

    This plugin creates audit log documents:

    • Actions: general interactions (GET).
    • Mutations: track old and new state of a resource (POST, PUT, DELETE), to effectively reason about state changes.

    For every request an event is emitted with an audit log (action or mutation) document.

    Quickstart

    await server.register({
        plugin: require("hapi-audit-rest"),
    });

    Example Audit Log Documents

    Consider a CRUD API on users.

    GET Requests

    // emitted data on GET /api/users?page=1&limit=10&sort=asc&column=id
    {
        application: "my-app",
        type: "SEARCH",
        body: {
            entity: "users",
            entityId: null,
            action: "SEARCH",
            username: null, // or the username if authenticated
            timestamp: "2021-02-13T18:11:25.917Z",
            data: {
                page: 1,
                limit: 10,
                sort: 'asc',
                column: 'id'
            },
        },
        outcome: "Success",
    };
    
    // emitted data on GET /api/users/1
    {
        application: "my-app",
        type: "SEARCH",
        body: {
            entity: "users",
            entityId: "1",
            action: "SEARCH",
            username: null, // or the username if authenticated
            timestamp: "2021-02-13T18:11:25.917Z",
            data: {},
        },
        outcome: "Success",
    };

    POST Requests

    // consider the payload
    const user = {
        username: "user",
        firstName: "first name",
        lastName: "last name",
    };
    
    // emitted data on POST /api/users, with payload user, created user with id returned in response
    {
        application: "my-app",
        type: "MUTATION",
        body: {
            entity: "users",
            entityId: 1,
            action: "CREATE",
            username: null, // or the username if authenticated
            originalValues: null,
            newValues: {
                id: 1,
                username: "user",
                firstName: "first name",
                lastName: "last name",
            },
            timestamp: "2021-02-20T20:53:04.821Z",
        },
        outcome: "Success",
    };

    DELETE Requests

    // emitted data on DELETE /api/users/1
    {
        application: "my-app",
        type: "MUTATION",
        body: {
            entity: "users",
            entityId: 1,
            action: "DELETE",
            username: null, // or the username if authenticated
            originalValues: {
                id: 1,
                username: "user",
                firstName: "first name",
                lastName: "last name",
            },
            newValues: null,
            timestamp: "2021-02-20T20:53:04.821Z",
        },
        outcome: "Success",
    };

    PUT Requests

    // consider the payload
    const user = {
        firstName: "updated first",
    };
    // emitted data on PUT /api/users/1
    {
        application: "my-app",
        type: "MUTATION",
        body: {
            entity: "users",
            entityId: 1,
            action: "UPDATE",
            username: null, // or the username if authenticated
            originalValues: {
                id: 1,
                username: "user",
                firstName: "first name",
                lastName: "last name",
            },
            newValues: {
                firstName: "updated first", // use option fetchNewValues for the whole updated entity object
            },
            timestamp: "2021-02-20T20:53:04.821Z",
        },
        outcome: "Success",
    };

    API

    Plugin registration options

    await server.register({
        plugin: require("hapi-audit-rest"),
        options: {
            // plugin registration options
        },
    });
    Name Type Default Mandatory Description
    auditGetRequests Boolean true no Enable/Disable auditing of GET requests.
    showErrorsOnStdErr Boolean true no Display errors on std error stream.
    diffFunc Function provided no External function to diff old and new values, Must return an array with two elements: old values and new values, with that order. The default implementation returns fetched old and new values.

    Signature
    function (oldValues, newValues) {return [oldValues, newValues]}
    cacheEnabled Boolean true no Enable/Disable internal cache. Use cache only if running an one instance server (default enabled). If a GET by id is triggered before an update (PUT), old values will be loaded from cache instead of requiring an extra GET by id API call.
    clientId String my-app no Application instance name or auth client id.
    auditAuthOnly Boolean false no Enable/Disable auditing of only authenticated requests.
    usernameKey String yes (when auditAuthOnly enabled)
    else no
    The path/key to the username stored in request.auth.credentials object.
    cacheExpiresIn Number Positive Integer 900000 (15mins) no Time (msecs) until cache expires (when cacheEnabled = false). Minimum 60000 (1 minute).
    isAuditable Function provided no Checks if current path is auditable. The default implementation checks if path starts with /api.

    Signature
    function (path, method) {return Boolean}
    eventHandler Function provided no Handler for the emitted events. The default implementations prints the audit log to stdout. You will have to implement this function in order to do something with the audit log.

    Signature
    function ({ auditLog, routeEndpoint })
    getEntity Function provided no Creates the entity name of the audit log. The default implementation returns the string after /api/ and before next / if any.

    Signature
    function (path) {return String}
    isEnabled Boolean true no Enable/Disable plugin initialization and functionality.

    Plugin route options

    // at any route
    options: {
       plugins: {
          "hapi-audit-rest": {
            // plugin route options
          }
       }
    }
    Name Type Default Mandatory Description
    ext Function no An extension point per route, invoked on pre-response, to customize audit log document values:
    • on GET
      async (request) => AuditAction
    • on POST
      async (request, { newVals }) => AuditMutation
    • on PUT
      async (request, { oldVals, newVals, diff }) => AuditMutation

      diff: ({diffOnly, skipDiff}) => [originalValues, newValues]
    • on DELETE
      async (request, { oldVals }) => AuditMutation
    • on PUT/POST and isAction=true
      async (request) => AuditAction
    Must return an object (AuditAction or AuditMutation) with any of the following properties to override the default values:
    • Audit Action
      • type String
      • entity String
      • entityId String/Number/Null
      • action String
      • data Object/Null
    • Audit Mutation
      • entity String
      • entityId String/Number/Null
      • action String
      • originalValues Object/Array/Null
      • newValues Object/Array/Null
    isAction Boolean false no Enable/Disable creation of action audit log documents for PUT/POST requests instead of mutation.
    getPath Function no On PUT requests, old and/or new values are fetched by injecting a GET by id request, based on PUT route path. When GET by id route path differs, it must be provided.

    Signature
    function (request) {return String}
    fetchNewValues Boolean false no On PUT requests, the incoming payload will be used as newValues. In case there are any model inconsistencies, this option will inject a GET by id request to fetch the newValues.

    Disable plugin on route

    By default the plugin applies to all registered routes. Should you need to exclude any, apply to the route:

    options: {
       plugins: {
          "hapi-audit-rest": false,
       },
    }

    Flows & Audit Log Data

    To effectively track old and new state of a resource, the plugin implements internal flows based on the following semantics:

    HTTP method Scope Description
    GET collection Retrieve all resources in a collection
    GET resource Retrieve a single resource
    POST collection Create a new resource in a collection
    PUT resource Update a resource
    DELETE resource Delete a resource

    The user can override audit log document defaults by using the route extension point.

    GET - scope collection

    An action audit log document is created, on pre-response lifecycle if the request succeeds with the following defaults:

    {
        application: "my-app",		// or the clientId if specified
        type: "SEARCH",
        body: {
            entity: $,				// as specified by getEntity function
            entityId: null,
            action: "SEARCH",
            username: null,			// or the username if authenticated
            timestamp: Date.now(),
            data: request.query,
        },
        outcome: "Success",
    };

    GET - scope resource

    An action audit log document is created, on pre-response lifecycle if the request succeeds with the following defaults:

    {
        application: "my-app",		// or the clientId if specified
        type: "SEARCH",
        body: {
            entity: $,				// as specified by getEntity function
            entityId: request.params.id,
            action: "SEARCH",
            username: null,			// or the username if authenticated
            timestamp: Date.now(),
            data: request.query,
        },
        outcome: "Success",
    };

    The response is cached if cashing enabled.

    POST - scope collection

    mutation (default)

    A mutation audit log document is created on pre-response lifecycle if the request succeeds with the following defaults:

    {
        application: "my-app",		// or the clientId if specified
        type: "MUTATION",
        body: {
            entity: $,				// as specified by getEntity function
            entityId: request.response.source.id || request.payload.id,
            action: "CREATE",
            username: null,			// or the username if authenticated
            originalValues: null,
            newValues: request.response.source || request.payload,	// the response or the payload if response null
            timestamp: Date.now()",
        },
        outcome: "Success",
    };
    • POST mutations rely to request payload or response payload to track the new resource state. If request is streamed to an upstream server this will result to an error.
    action

    In cases that it is not meaningful to audit a mutation, an action audit log document can be created by setting isAction route parameter.

    {
        application: "my-app",		// or the clientId if specified
        type: "SEARCH",
        body: {
            entity: $,				// as specified by getEntity function
            entityId: request.params.id || request.payload.id,
            action: "SEARCH",
            username: null,			// or the username if authenticated
            timestamp: Date.now(),
            data: request.payload,	// or null if request streamed
        },
        outcome: "Success",
    };

    PUT - scope resource

    mutation (default)

    A mutation audit log document is created on pre-response lifecycle if the request succeeds with the following defaults:

    {
        application: "my-app",		// or the clientId if specified
        type: "MUTATION",
        body: {
            entity: $,				// as specified by getEntity function
            entityId: request.params.id || newValues.id,	// where newValues is either the request payload (default) or the resource data fetched after update when fetchNewValues=true or request streamed
            action: "UPDATE",
            username: null,			// or the username if authenticated
            originalValues: $,		// values fetched with injected GET by id call (or loaded from cache)
            newValues: request.payload || newValues,	// newValues = values fetched by injected GET by id call when fetchNewValues=true or request streamed
            timestamp: Date.now()",
        },
        outcome: "Success",
    };

    PUT mutations are the most complex.

    • Before the update, the original resource state is retrieved by inspecting the cache. If not in cache a GET by id request is injected based on the current request path (custom path can be set on route with getPath).
    • After the update, the new resource state is retrieved from the request payload. If the request is streamed or the fetchNewValues option is set, a GET by id request will be injected to fetch the new resource state.
    action

    In cases that it is not meaningful to audit a mutation, an action audit log document can be created by setting isAction route parameter.

    {
        application: "my-app",		// or the clientId if specified
        type: "SEARCH",
        body: {
            entity: $,				// as specified by getEntity function
            entityId: request.params.id || request.payload.id,
            action: "SEARCH",
            username: null,			// or the username if authenticated
            timestamp: Date.now(),
            data: request.payload,	// or null if request streamed
        },
        outcome: "Success",
    };

    DELETE - scope resource

    A mutation audit log document is created on pre-response lifecycle if the request succeeds with the following defaults:

    {
        application: "my-app",		// or the clientId if specified
        type: "MUTATION",
        body: {
            entity: $,				// as specified by getEntity function
            entityId: request.params.id || originalValues.id,	// where originalValues = resource state before delete
            action: "DELETE",
            username: null,			// or the username if authenticated
            originalValues: $,		// values fetched with injected GET by id request before delete
            newValues: null,
            timestamp: Date.now()",
        },
        outcome: "Success",
    };

    DELETE mutations retrieve old resource state by injecting a GET by id request before the delete operation.

    Error handling

    When an error occurs, it is logged using the request.log(tags, [data]) method:

    • tags: "error", "hapi-audit-rest"
    • data: error.message

    The server isntance can interact with log information:

    server.events.on({ name: "request", channels: "app" }, (request, event, tags) => {
        if (tags.error && tags["hapi-audit-rest"]) {
            console.log(event); // do something with error data
        }
    });

    If showErrorsOnStdErr option is enabled (default), the error message will be printed to stderr for convenience.

    License

    hapi-audit-rest is licensed under a MIT License.

    Install

    npm i hapi-audit-rest

    DownloadsWeekly Downloads

    3

    Version

    3.5.0

    License

    MIT

    Unpacked Size

    129 kB

    Total Files

    33

    Last publish

    Collaborators

    • avatar