npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

hapi-audit-rest

v4.6.0

Published

A NPM package to audit rest API calls on a Hapi.js server

Downloads

300

Readme

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.js v18 or higher, Node.js v14 or higher. For compatibility with Node.js v12 check version 3.

Installation

npm i -S hapi-audit-rest

Testing

npm test

About

This plugin creates audit log documents based on REST semantics.

| HTTP method | Description | Audit Log Document | | ----------- | ----------------------- | ------------------ | | GET | Retrieve resources | Action | | POST | Create a new resource | Mutation - Create | | PUT | Update a resource | Mutation - Update | | DELETE | Delete a resource | Mutation - Delete |

Mutations track old and new state of a resource to effectively reason about state changes.

For every request an event is emitted with an audit log document.

Quickstart

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

Audit Log Document Schemas

Action Schema

{
    application: String
    type: String,
    body: {
        entity: String,
        entityId: String|Number|Null,
        action: String,
        username: String|Null,
        data: Object|Null,
        timestamp: String,
    },
    outcome: String
};

Mutation Schema

{
    application: String
    type: String,
    body: {
        entity: String,
        entityId: String|Number|Null,
        action: String,
        username: String|Null,
        originalValues: Object|Array|Null,
        newValues: Object|Array|Null,
        timestamp: String,
    },
    outcome: String
};

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: "/api/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: "/api/users/1",
        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: "/api/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: "/api/users/1",
        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: "/api/users/1",
        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 | | ------------------ | ------------------------- | --------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | debug | Boolean | false | 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]} | | isCacheEnabled | 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. | | usernameKey | String | | 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 request is auditable. The default implementation audits all requests.Signature function (request) {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 }) | | setEntity | Function | provided | no | Creates the entity name of the audit log. The default implementation returns the endpoint path.Signature function (path) {return String} | | isEnabled | Boolean | true | no | Enable/Disable plugin initialization and functionality. | | extAll | Function | - | no | A global override entrypoint to extend any value of any created audit log document, invoked on pre-response. Signature function (request, auditLog) {return Object} |

Handle common cases

Common use cases for isAuditable option:

await server.register({
    plugin: require("hapi-audit-rest"),
    options: {
        isAuditable: ({ auth: { isAuthenticated }, method, url: { pathname } }) => {
            // do not audit unauthenticated requests
            if (!isAuthenticated) {
                return false
            }
            
            // do not audit GET requests
            if (method === "get") {
                return false
            }
            
            // do not audit requests when path does not start from /api
            if (!pathname.startsWith("/api")) {
                return false
            }
            
            // return true to audit all other cases
            return true
        }
    },
});

Common use cases for setEntity option:

await server.register({
    plugin: require("hapi-audit-rest"),
    options: {
        // use the standard pattern of an api i.e. /api/v1.0/users, to refine the entity name
        // will have 'entity: users' in audit log
        setEntity: (path) => path.split("/")[3], 
        }
    },
});

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 GETasync (request) => AuditActionon POSTasync (request, { newVals }) => AuditMutationon PUTasync (request, { oldVals, newVals, diff }) => AuditMutationdiff: ({diffOnly, skipDiff}) => [originalValues, newValues] on DELETEasync (request, { oldVals }) => AuditMutationon PUT/POST/DELETE and isAction=true async (request) => AuditActionMust return an object (AuditAction or AuditMutation) with any of the following properties to override the default values: Audit Actiontype Stringentity StringentityId String/Number/Nullaction Stringdata Object/NullAudit Mutationentity StringentityId String/Number/Nullaction StringoriginalValues Object/Array/NullnewValues Object/Array/Null | | isAction | Boolean | false | no | Enable/Disable creation of action audit log documents for PUT/POST/DELETE requests instead of mutation. | | setInjectedPath | 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 | resource | Create a new resource in a collection | | PUT | resource | Update a resource | | DELETE | resource | Delete a resource |

To override audit log document defaults use the route extension point. To completely override any created audit log document use the global override registration option extend all.

GET - scope collection

get_collection_flow

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 setEntity function
        entityId: null,
        action: "SEARCH",
        username: null,			// or the username if authenticated
        timestamp: Date.now(),
        data: request.query,
    },
    outcome: "Success",
};

GET - scope resource

get_resource_flow

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 setEntity 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 resource

mutation (default)

post_resource_mutation_flow_2

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 setEntity 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

post_put_delete_resource_action_flow_2

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 setEntity 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)

put_resource_mutation_flow_2

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 setEntity 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 setInjectedPath).
  • 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 setEntity 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

mutation (default)

delete_resource_mutation_flow

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 setEntity 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.

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 setEntity 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",
};

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 debug option is enabled (disabled by default), the error message will be printed to stderr for convenience.

License

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