diff --git a/README.md b/README.md index dbc8bb2..9180263 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ -# owasp-logging +# owasp-helpers -This package implements the OWASP Cheat Sheet for [Application Logging Vocabulary](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#input-validation-input), a standard vocabulary for logging security events. +This package is intended to assist developers to follow OWASP best practices. + +Currently, it implements the OWASP Cheat Sheet for [Application Logging Vocabulary](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#input-validation-input), a standard vocabulary for logging security events. The intent is to simplify monitoring and alerting such that, assuming developers trap errors and log them using this vocabulary, monitoring and alerting would be improved by simply keying on these terms. @@ -9,14 +11,16 @@ This logging standard would seek to define specific keywords which, when applied ## Installation ```bash -npm install owasp-logging -# yarn add owasp-logging -# pnpm install owasp-logging -# bun install owasp-logging +npm install owasp +# yarn add owasp +# pnpm install owasp +# bun install owasp ``` ## Usage +### Logging vocabulary + Here is an example of how to use this package with [pino](https://github.com/pinojs/pino) and [Express](https://github.com/expressjs/express) to log authentication events. @@ -27,9 +31,9 @@ and [Express](https://github.com/expressjs/express) to log authentication events ```ts import { Router } from 'express'; -import { authn_login_fail, authn_login_fail_max, authn_login_success } from 'owasp-logging'; +import { authn_login_fail, authn_login_fail_max, authn_login_success } from 'owasp/vocab'; // Or, if you want to simplify imports, you can do: -// import * as owasp from 'owasp-logging'; +// import * as owasp from 'owasp-helpers'; import { logger as rootLogger } from '../logger.js'; const router = Router(); @@ -48,7 +52,7 @@ router.route("/login").post(async (req, res, next) => { if (!userId || !password || userId.length === 0 || password.length === 0) { logger.warn( { - // owasp-logging provides a set of standard events to log. + // owasp-helpers provides a set of standard events to log. // Use the `event` property to log the event. event: authn_login_fail(userId), // The result of this function is: `authn_login_fail:${userId}` }, @@ -152,5 +156,5 @@ Ensure linting, formatting, and tests pass before submitting a PR. ```bash bun run check -bun test +bun test # let's keep the test coverage at 100%! ``` diff --git a/bun.lockb b/bun.lockb index 9381107..db07c53 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 448b1b2..b72ef6e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "owasp-logging", + "name": "owasp", "version": "1.0.0", "type": "module", "license": "MIT", @@ -9,9 +9,9 @@ "author": { "name": "Quinn Turner" }, - "homepage": "https://github.com/quinnturner/owasp-logging#readme", + "homepage": "https://github.com/quinnturner/owasp#readme", "bugs": { - "url": "https://github.com/quinnturner/owasp-logging/issues" + "url": "https://github.com/quinnturner/owasp/issues" }, "keywords": [ "owasp", @@ -19,14 +19,14 @@ "security" ], "exports": { - ".": { + "./vocab": { "import": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" + "types": "./dist/vocab.d.ts", + "default": "./dist/vocab.js" }, "require": { - "types": "./dist/index.d.cts", - "default": "./dist/index.cjs" + "types": "./dist/vocab.d.cts", + "default": "./dist/vocab.cjs" } }, "./package.json": "./package.json" @@ -43,7 +43,7 @@ "build": "tsup --config tsup.config.ts" }, "devDependencies": { - "@biomejs/biome": "^1.4.0", + "@biomejs/biome": "^1.4.1", "bun-types": "^1.0.14", "tsup": "^8.0.1", "type-fest": "^4.8.2", diff --git a/src/index.ts b/src/index.ts index 894d522..3dcd380 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1 @@ -export * from "./authentication.js"; -export * from "./authorization.js"; -export * from "./excessive-use.js"; -export * from "./file-upload.js"; +export * from "./vocab/index.js"; diff --git a/src/authentication.test.ts b/src/vocab/authentication.test.ts similarity index 100% rename from src/authentication.test.ts rename to src/vocab/authentication.test.ts diff --git a/src/authentication.ts b/src/vocab/authentication.ts similarity index 83% rename from src/authentication.ts rename to src/vocab/authentication.ts index 26020ab..74f10d4 100644 --- a/src/authentication.ts +++ b/src/vocab/authentication.ts @@ -21,7 +21,7 @@ import type { Join } from "type-fest"; * "requestId": "4c682970-ef75-4605-93f2-ab7cf5316d83" * } * ``` - * @see https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authn_login_successuserid + * @see [OWASP Logging Vocabulary Cheat Sheet - authn_login_success](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authn_login_successuserid) */ export function authn_login_success( userId: U, @@ -51,7 +51,7 @@ export function authn_login_success( * "requestId": "b2b0fc16-cfc1-42cc-a1a7-102a11e7fa6e" * } * ``` - * @see https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authn_login_successafterfailuseridretries + * @see [OWASP Logging Vocabulary Cheat Sheet - authn_login_successafterfail](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authn_login_successafterfailuseridretries) */ export function authn_login_successafterfail< U extends string, @@ -82,7 +82,7 @@ export function authn_login_successafterfail< * "requestId": "b7c29e30-199e-4234-a8f1-fff0c12f1624" * } * ``` - * @see https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authn_login_failuserid + * @see [OWASP Logging Vocabulary Cheat Sheet - authn_login_fail](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authn_login_failuserid) */ export function authn_login_fail( userId: U, @@ -112,7 +112,7 @@ export function authn_login_fail( * "requestId": "b7c29e30-199e-4234-a8f1-fff0c12f1624" * } * ``` - * @see https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authn_login_fail_maxuseridmaxlimitint + * @see [OWASP Logging Vocabulary Cheat Sheet - authn_login_fail_max](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authn_login_fail_maxuseridmaxlimitint) */ export function authn_login_fail_max( userId: U, @@ -159,7 +159,7 @@ export function authn_login_fail_max< * "requestId": "e6a7c70a-7972-49c9-a056-1af31cecf334" * } * ``` - * @see https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authn_login_lockuseridreason + * @see [OWASP Logging Vocabulary Cheat Sheet - authn_login_lock](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authn_login_lockuseridreason) */ export function authn_login_lock< U extends string | number | bigint, @@ -198,7 +198,7 @@ export function authn_login_lock< * "requestId": "72b29ffe-4b2a-4eef-9b4b-e9dc36483d28" * } * ``` - * @see https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authn_password_changeuserid + * @see [OWASP Logging Vocabulary Cheat Sheet - authn_password_change](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authn_password_changeuserid) */ export function authn_password_change( userId: U, @@ -227,7 +227,7 @@ export function authn_password_change( * "requestId": "e5c80286-e7b4-4add-ab67-24ce28c7f187" * } * ``` - * @see https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authn_password_change_failuserid + * @see [OWASP Logging Vocabulary Cheat Sheet - authn_password_change_fail](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authn_password_change_failuserid) */ export function authn_password_change_fail( userId: U, @@ -258,7 +258,7 @@ export function authn_password_change_fail( * "requestId": "c1a514a9-70f1-4496-8c69-ebf9337783f6" * } * ``` - * @see https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authn_impossible_traveluseridregion1region2 + * @see [OWASP Logging Vocabulary Cheat Sheet - authn_impossible_travel](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authn_impossible_traveluseridregion1region2) */ export function authn_impossible_travel< U extends string | number | bigint, @@ -290,7 +290,7 @@ export function authn_impossible_travel< * "requestId": "e6002b7c-87f4-45c9-abf6-f1e0f94523d5" * } * ``` - * @see https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authn_token_createduserid-entitlements + * @see [OWASP Logging Vocabulary Cheat Sheet - authn_token_created](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authn_token_createduserid-entitlements) */ export function authn_token_created< U extends string | number | bigint, @@ -323,7 +323,7 @@ export function authn_token_created< * "requestId": "caa98ad0-4b9c-4b20-8633-6be104b1933b" * } * ``` - * @see https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authn_token_revokeduseridtokenid + * @see [OWASP Logging Vocabulary Cheat Sheet - authn_token_revoked](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authn_token_revokeduseridtokenid) */ export function authn_token_revoked( userId: U, @@ -363,7 +363,7 @@ export function authn_token_revoked< * "requestId": "1cfdc054-b87a-4533-9c9a-d329bca0da44" * } * ``` - * @see https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authn_token_reuseuseridtokenid + * @see [OWASP Logging Vocabulary Cheat Sheet - authn_token_reuse](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authn_token_reuseuseridtokenid) */ export function authn_token_reuse( userId: U, @@ -402,7 +402,7 @@ export function authn_token_reuse< * "requestId": "ecd6341b-bc7c-4301-9c75-f49427f59406" * } * ``` - * @see https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authn_token_deleteappid + * @see [OWASP Logging Vocabulary Cheat Sheet - authn_token_delete](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authn_token_deleteappid) */ export function authn_token_delete( appId: A, diff --git a/src/authorization.test.ts b/src/vocab/authorization.test.ts similarity index 100% rename from src/authorization.test.ts rename to src/vocab/authorization.test.ts diff --git a/src/authorization.ts b/src/vocab/authorization.ts similarity index 83% rename from src/authorization.ts rename to src/vocab/authorization.ts index 2e2d643..d573966 100644 --- a/src/authorization.ts +++ b/src/vocab/authorization.ts @@ -20,7 +20,7 @@ * "requestId": "00b13b12-51ab-49bc-94be-34c450804850" * } * ``` - * @see https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authz_failuseridresource + * @see [OWASP Logging Vocabulary Cheat Sheet - authz_fail](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authz_failuseridresource) */ export function authz_fail< U extends string | number | bigint, @@ -52,7 +52,7 @@ export function authz_fail< * "requestId": "5e952d3b-97b6-4c20-a241-b3aa9a591647" * } * ``` - * @see https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authz_changeuseridfromto + * @see [OWASP Logging Vocabulary Cheat Sheet - authz_change](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authz_changeuseridfromto) */ export function authz_change< U extends string | number | bigint, @@ -87,7 +87,7 @@ export function authz_change< * "requestId": "e02ffa1d-99b6-4616-b54e-5f51ed208331" * } * ``` - * @see https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authz_changeuseridfromto + * @see [OWASP Logging Vocabulary Cheat Sheet - authz_admin](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#authz_changeuseridfromto) */ export function authz_admin< U extends string | number | bigint, diff --git a/src/excessive-use.test.ts b/src/vocab/excessive-use.test.ts similarity index 100% rename from src/excessive-use.test.ts rename to src/vocab/excessive-use.test.ts diff --git a/src/excessive-use.ts b/src/vocab/excessive-use.ts similarity index 82% rename from src/excessive-use.ts rename to src/vocab/excessive-use.ts index 1f0660c..4bd6b05 100644 --- a/src/excessive-use.ts +++ b/src/vocab/excessive-use.ts @@ -20,7 +20,7 @@ * "requestId": "e997c333-fd0a-4880-b6d0-27e871285e50" * } * ``` - * @see https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#excess_rate_limit_exceededuseridmax + * @see [OWASP Logging Vocabulary Cheat Sheet - excess_rate_limit_exceeded](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#excess_rate_limit_exceededuseridmax) */ export function excess_rate_limit_exceeded< U extends string | number | bigint, diff --git a/src/file-upload.test.ts b/src/vocab/file-upload.test.ts similarity index 100% rename from src/file-upload.test.ts rename to src/vocab/file-upload.test.ts diff --git a/src/file-upload.ts b/src/vocab/file-upload.ts similarity index 86% rename from src/file-upload.ts rename to src/vocab/file-upload.ts index 66b8fc8..2dc8bd3 100644 --- a/src/file-upload.ts +++ b/src/vocab/file-upload.ts @@ -21,7 +21,7 @@ * "requestId": "6e48278f-5f2c-4af0-b821-9d83f1cce30b" * } * ``` - * @see https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#upload_completeuseridfilenametype + * @see [OWASP Logging Vocabulary Cheat Sheet - upload_complete](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#upload_completeuseridfilenametype) */ export function upload_complete< U extends string | number | bigint, @@ -64,7 +64,7 @@ export function upload_complete< * "requestId": "7d1fbc36-fc8b-4dc3-8e79-841216f48ada" * } * ``` - * @see https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#upload_storedfilenamefromto + * @see [OWASP Logging Vocabulary Cheat Sheet - upload_stored](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#upload_storedfilenamefromto) */ export function upload_stored( filename: N, @@ -105,7 +105,7 @@ export function upload_stored( * "requestId": "46f449eb-bd37-43ef-a5a6-8d76d80b8975" * } * ``` - * @see https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#upload_validationfilenamevirusscanimagemagickfailedincompletepassed + * @see [OWASP Logging Vocabulary Cheat Sheet - upload_validation](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#upload_validationfilenamevirusscanimagemagickfailedincompletepassed) */ export function upload_validation< N extends string, @@ -137,7 +137,7 @@ export function upload_validation< * "requestId": "d4fc4479-210b-4d89-ae1e-4fe7b595cbb4" * } * ``` - * @see https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#upload_deleteuseridfileid + * @see [OWASP Logging Vocabulary Cheat Sheet - upload_delete](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#upload_deleteuseridfileid) */ export function upload_delete< U extends string | number | bigint, diff --git a/src/vocab/index.ts b/src/vocab/index.ts new file mode 100644 index 0000000..6c8286c --- /dev/null +++ b/src/vocab/index.ts @@ -0,0 +1,12 @@ +export * from "./authentication.js"; +export * from "./authorization.js"; +export * from "./excessive-use.js"; +export * from "./file-upload.js"; +export * from "./input-validation.js"; +export * from "./malicious-behavior.js"; +export * from "./privilege-changes.js"; +export * from "./sensitive-data-changes.js"; +export * from "./sequence-errors.js"; +export * from "./session-management.js"; +export * from "./system-events.js"; +export * from "./user-management.js"; diff --git a/src/vocab/input-validation.test.ts b/src/vocab/input-validation.test.ts new file mode 100644 index 0000000..07ee309 --- /dev/null +++ b/src/vocab/input-validation.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, it } from "bun:test"; +import { input_validation_fail } from "./input-validation"; + +describe("input_validation_fail", () => { + it("should return the correct string", () => { + const result = input_validation_fail("field1", "user123"); + expect(result).toBe("input_validation_fail:field1,user123"); + }); +}); diff --git a/src/vocab/input-validation.ts b/src/vocab/input-validation.ts new file mode 100644 index 0000000..d12390d --- /dev/null +++ b/src/vocab/input-validation.ts @@ -0,0 +1,33 @@ +/** + * When input validation fails on the server-side it must either be because + * a) sufficient validation was not provided on the client, or + * b) client-side validation was bypassed. + * In either case it's an opportunity for attack and should be mitigated quickly. + * + * **Level:** `WARN` + * + * @example + * ```ts + * const field = 'date_of_birth'; + * const userId = "joebob1"; + * logger.warn({ event: input_validation_fail(field, userId) }, `User ${userId} submitted data that failed validation.`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "input_validation_fail:date_of_birth,joebob1", + * "level": "WARN", + * "description": "User joebob1 submitted data that failed validation.", + * "requestId": "e22569d2-5fb6-4453-ae7e-c496a2584d94" + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - input_validation_fail](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#input-validation-input) + */ +export function input_validation_fail< + F extends string, + U extends string | number | bigint, +>(field: F, userId: U) { + return `input_validation_fail:${field},${userId}` as const; +} diff --git a/src/vocab/malicious-behavior.test.ts b/src/vocab/malicious-behavior.test.ts new file mode 100644 index 0000000..06fbfee --- /dev/null +++ b/src/vocab/malicious-behavior.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, it } from "bun:test"; +import { + malicious_attack_tool, + malicious_cors, + malicious_direct_reference, + malicious_excess_404, + malicious_extraneous, +} from "./malicious-behavior.js"; + +describe("malicious_extraneous", () => { + it("should return the correct string", () => { + const result = malicious_extraneous("user123", "input1", "Mozilla/5.0"); + expect(result).toBe("malicious_extraneous:user123,input1,Mozilla/5.0"); + }); + + it("should handle numeric userIdOrIp", () => { + const result = malicious_extraneous(456, "input2", "Chrome/91.0.4472.124"); + expect(result).toBe("malicious_extraneous:456,input2,Chrome/91.0.4472.124"); + }); + + it("should handle bigint userIdOrIp", () => { + const result = malicious_extraneous(BigInt(789), "input3", "Safari/14.1.1"); + expect(result).toBe("malicious_extraneous:789,input3,Safari/14.1.1"); + }); +}); +describe("malicious_attack_tool", () => { + it("should return the correct string with string userIdOrIp, string toolName, and string userAgent", () => { + const result = malicious_attack_tool("user123", "tool1", "Mozilla/5.0"); + expect(result).toBe("malicious_attack_tool:user123,tool1,Mozilla/5.0"); + }); + + it("should return the correct string with number userIdOrIp, string toolName, and string userAgent", () => { + const result = malicious_attack_tool(123, "tool2", "Chrome/91.0.4472.124"); + expect(result).toBe("malicious_attack_tool:123,tool2,Chrome/91.0.4472.124"); + }); + + it("should return the correct string with bigint userIdOrIp, string toolName, and string userAgent", () => { + const result = malicious_attack_tool(BigInt(456), "tool3", "Safari/14.1.1"); + expect(result).toBe("malicious_attack_tool:456,tool3,Safari/14.1.1"); + }); +}); +describe("malicious_excess_404", () => { + it("should return the correct string", () => { + const userIdOrIp = "123"; + const userAgent = "Mozilla/5.0"; + const result = malicious_excess_404(userIdOrIp, userAgent); + expect(result).toBe("malicious_excess404:123,Mozilla/5.0"); + }); + + it("should handle numeric userIdOrIp", () => { + const userIdOrIp = 456; + const userAgent = "Chrome/91.0.4472.124"; + const result = malicious_excess_404(userIdOrIp, userAgent); + expect(result).toBe("malicious_excess404:456,Chrome/91.0.4472.124"); + }); + + it("should handle bigint userIdOrIp", () => { + const userIdOrIp = BigInt(789); + const userAgent = "Safari/14.1.1"; + const result = malicious_excess_404(userIdOrIp, userAgent); + expect(result).toBe("malicious_excess404:789,Safari/14.1.1"); + }); +}); +describe("malicious_cors", () => { + it("should return the correct string", () => { + const userIdOrIp = "user123"; + const userAgent = "Mozilla/5.0"; + const referer = "https://example.com"; + const result = malicious_cors(userIdOrIp, userAgent, referer); + expect(result).toBe( + "malicious_cors:user123,Mozilla/5.0,https://example.com", + ); + }); + + it("should handle numeric userIdOrIp", () => { + const userIdOrIp = 456; + const userAgent = "Chrome/91.0.4472.124"; + const referer = "https://example.com"; + const result = malicious_cors(userIdOrIp, userAgent, referer); + expect(result).toBe( + "malicious_cors:456,Chrome/91.0.4472.124,https://example.com", + ); + }); + + it("should handle bigint userIdOrIp", () => { + const userIdOrIp = BigInt(789); + const userAgent = "Safari/14.1.1"; + const referer = "https://example.com"; + const result = malicious_cors(userIdOrIp, userAgent, referer); + expect(result).toBe("malicious_cors:789,Safari/14.1.1,https://example.com"); + }); +}); +describe("malicious_direct_reference", () => { + it("should return the correct string with string userIdOrIp and string userAgent", () => { + const result = malicious_direct_reference("user123", "Mozilla/5.0"); + expect(result).toBe("malicious_direct:user123,Mozilla/5.0"); + }); + + it("should return the correct string with number userIdOrIp and string userAgent", () => { + const result = malicious_direct_reference(123, "Chrome/91.0.4472.124"); + expect(result).toBe("malicious_direct:123,Chrome/91.0.4472.124"); + }); + + it("should return the correct string with bigint userIdOrIp and string userAgent", () => { + const result = malicious_direct_reference(BigInt(456), "Safari/14.1.1"); + expect(result).toBe("malicious_direct:456,Safari/14.1.1"); + }); +}); diff --git a/src/vocab/malicious-behavior.ts b/src/vocab/malicious-behavior.ts new file mode 100644 index 0000000..d00cf59 --- /dev/null +++ b/src/vocab/malicious-behavior.ts @@ -0,0 +1,167 @@ +/** + * When a user makes numerous requests for files that don't exist it often is an indicator of attempts to "force-browse" for files that could exist and is often behavior indicating malicious intent. + * + * **Level:** `WARN` + * + * @example + * ```ts + * const userId = "joebob1"; // or req.ip if you don't have a userId + * const ua = req.get("User-Agent"); + * logger.warn({ event: malicious_excess_404(userId, ua) }, `A user at ${ip} has generated a large number of 404 requests.`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "malicious_excess404:123.456.789.101,M@l1c10us-Hax0rB0t0-v1", + * "level": "WARN", + * "description": "A user at 123.456.789.101 has generated a large number of 404 requests.", + * "requestId": "e22569d2-5fb6-4453-ae7e-c496a2584d94" + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - malicious_excess_404](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#malicious_excess_404useridipuseragent) + */ +export function malicious_excess_404< + U extends string | number | bigint, + UA extends string, +>(userIdOrIp: U, userAgent: UA) { + return `malicious_excess404:${userIdOrIp},${userAgent}` as const; +} + +/** + * When a user submits data to a backend handler that was not expected it can indicate probing for input validation errors. + * If your backend service receives data it does not handle or have an input for this is an indication of likely malicious abuse. + * + * **Level:** `WARN` + * + * @example + * ```ts + * const userId = "joebob1"; // or req.ip if you don't have a userId + * const inputName = "creditcardnum"; + * const ua = req.get("User-Agent"); + * logger.warn({ event: malicious_extraneous(userId, ua) }, `A user at ${ip} has generated a large number of 404 requests.`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "malicious_extraneous:dr@evil.com,creditcardnum,Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0", + * "level": "WARN", + * "description": "User dr@evil.com included field creditcardnum in the request which is not handled by this service.", + * "requestId": "3f63d196-7699-4f14-ab44-7597bd13dd5d" + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - malicious_extraneous](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#malicious_extraneoususeridipinputnameuseragent) + */ +export function malicious_extraneous< + U extends string | number | bigint, + I extends string, + UA extends string, +>(userIdOrIp: U, inputName: I, userAgent: UA) { + return `malicious_extraneous:${userIdOrIp},${inputName},${userAgent}` as const; +} + +/** + * When obvious attack tools are identified either by signature or by user agent they should be logged. + * + * **Level:** `CRITICAL` + * + * @example + * ```ts + * const userId = "joebob1"; // or req.ip if you don't have a userId + * const toolName = "nikto"; + * const ua = req.get("User-Agent"); + * logger.critical({ event: malicious_attack_tool(userId, toolName, ua) }, `Attack traffic indicating use of ${toolName} coming from ${ip}`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "malicious_attack_tool:123.456.789.101,nikto,Mozilla/5.00 (Nikto/2.1.6) (Evasions:None) (Test:Port Check)", + * "level": "WARN", + * "description": "Attack traffic indicating use of Nikto coming from 123.456.789.101", + * "requestId": "e07d0d81-0ac4-4ef0-bcb4-100ff2c47486" + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - malicious_attack_tool](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#malicious_attack_tooluseridiptoolnameuseragent) + */ +export function malicious_attack_tool< + U extends string | number | bigint, + T extends string, + UA extends string, +>(userIdOrIp: U, toolName: T, userAgent: UA) { + return `malicious_attack_tool:${userIdOrIp},${toolName},${userAgent}` as const; +} + +/** + * When attempts are made from unauthorized origins they should of course be blocked, but also logged whenever possible. + * Even if we block an illegal cross-origin request the fact that the request is being made could be an indication of attack. + * + * > NOTE: Did you know that the word "referer" is misspelled in the original HTTP specification? + * > The correct spelling should be "referrer" but the original typo persists to this day and is used here intentionally. + * + * **Level:** `CRITICAL` + * + * @example + * ```ts + * const userId = "joebob1"; // or req.ip if you don't have a userId + * const ua = req.get("User-Agent"); + * const referer = req.get("Referer"); + * logger.warn({ event: malicious_cors(userId, ua, referer) }, `An illegal cross-origin request from ${ip} was referred from ${referer}`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "malicious_cors:127.0.0.1,Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0,attack.evil.com", + * "level": "WARN", + * "description": "An illegal cross-origin request from 127.0.0.1 was referred from attack.evil.com", + * "requestId": "303b3203-c108-4df1-a136-99c244b6d211" + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - malicious_cors](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#malicious_corsuseridipuseragentreferer) + */ +export function malicious_cors< + U extends string | number | bigint, + UA extends string, + R extends string, +>(userIdOrIp: U, userAgent: UA, referer: R) { + return `malicious_cors:${userIdOrIp},${userAgent},${referer}` as const; +} + +/** + * A common attack against authentication and authorization is to directly access an object without credentials or appropriate access authority. + * Failing to prevent this flaw used to be one of the OWASP Top Ten called **Insecure Direct Object Reference**. + * Assuming you've correctly prevented this attack, logging the attempt is valuable to identify malicious users. + * + * **Level:** `CRITICAL` + * + * @example + * ```ts + * const userId = "joebob1"; // or req.ip if you don't have a userId + * const ua = req.get("User-Agent"); + * logger.warn({ event: malicious_direct_reference(userId, ua) }, `User ${userId} attempted to access an object to which they are not authorized`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "malicious_direct:joebob1, Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0", + * "level": "WARN", + * "description": "User joebob1 attempted to access an object to which they are not authorized", + * "requestId": "e21196dc-ab3e-4911-b720-b796a374e5d6" + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - malicious_direct_reference](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#malicious_direct_referenceuseridip-useragent) + */ +export function malicious_direct_reference< + U extends string | number | bigint, + UA extends string, +>(userIdOrIp: U, userAgent: UA) { + return `malicious_direct:${userIdOrIp},${userAgent}` as const; +} diff --git a/src/vocab/privilege-changes.test.ts b/src/vocab/privilege-changes.test.ts new file mode 100644 index 0000000..6da65a3 --- /dev/null +++ b/src/vocab/privilege-changes.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "bun:test"; +import { privilege_permissions_changed } from "./privilege-changes"; + +describe("privilege_permissions_changed", () => { + it("should return the correct string", () => { + const result = privilege_permissions_changed( + "user123", + "file1", + "read", + "write", + ); + expect(result).toBe("malicious_direct:user123, file1,read,write"); + }); +}); diff --git a/src/vocab/privilege-changes.ts b/src/vocab/privilege-changes.ts new file mode 100644 index 0000000..40fc1b6 --- /dev/null +++ b/src/vocab/privilege-changes.ts @@ -0,0 +1,36 @@ +/** + * Tracking changes to objects to which there are access control restrictions can uncover attempt + * to escalate privilege on those files by unauthorized users. + * + * **Level:** `WARN` + * + * @example + * ```ts + * const userId = "joebob1"; + * const fileOrObject = "/users/admin/some/important/path"; + * const fromLevel = "0511"; + * const toLevel = "0777"; + * logger.warn({ event: privilege_permissions_changed(userId, fileOrObject, fromLevel, toLevel) }, `User ${userId} changed permissions on ${fileOrObject}`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "malicious_direct:joebob1, /users/admin/some/important/path,0511,0777", + * "level": "WARN", + * "description": "User joebob1 changed permissions on /users/admin/some/important/path", + * "requestId": "e21196dc-ab3e-4911-b720-b796a374e5d6" + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - privilege_permissions_changed](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#privilege_permissions_changeduseridfileobjectfromleveltolevel) + */ +export function privilege_permissions_changed< + U extends string | number | bigint, + F extends string, + FL extends string, + TL extends string, +>(userId: U, fileOrObject: F, fromLevel: FL, toLevel: TL) { + // The space is intentional, based on the OWASP example. + return `malicious_direct:${userId}, ${fileOrObject},${fromLevel},${toLevel}` as const; +} diff --git a/src/vocab/sensitive-data-changes.test.ts b/src/vocab/sensitive-data-changes.test.ts new file mode 100644 index 0000000..32fddbf --- /dev/null +++ b/src/vocab/sensitive-data-changes.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, it } from "bun:test"; +import { + sensitive_create, + sensitive_delete, + sensitive_read, + sensitive_update, +} from "./sensitive-data-changes.js"; + +describe("sensitive_create", () => { + it("should return the correct string", () => { + const result = sensitive_create("user123", "file1"); + expect(result).toBe("sensitive_create:user123, file1"); + }); +}); +describe("sensitive_read", () => { + it("should return the correct sensitive read string", () => { + const userId = "123"; + const fileOrObject = "example.txt"; + const result = sensitive_read(userId, fileOrObject); + const expected = "sensitive_read:123, example.txt"; + + expect(result).toBe(expected); + }); + + it("should handle numeric userId and fileOrObject", () => { + const userId = 456; + const fileOrObject = "data.json"; + const result = sensitive_read(userId, fileOrObject); + const expected = "sensitive_read:456, data.json"; + + expect(result).toBe(expected); + }); + + it("should handle bigint userId", () => { + const userId = BigInt(789); + const fileOrObject = "image.jpg"; + const result = sensitive_read(userId, fileOrObject); + const expected = "sensitive_read:789, image.jpg"; + + expect(result).toBe(expected); + }); +}); +describe("sensitive_update", () => { + it("should return the correct string", () => { + const userId = "123"; + const fileOrObject = "file.txt"; + const result = sensitive_update(userId, fileOrObject); + expect(result).toBe("sensitive_update:123, file.txt"); + }); + + it("should handle numeric userId", () => { + const userId = 456; + const fileOrObject = "object"; + const result = sensitive_update(userId, fileOrObject); + expect(result).toBe("sensitive_update:456, object"); + }); + + it("should handle bigint userId", () => { + const userId = BigInt(789); + const fileOrObject = "file.txt"; + const result = sensitive_update(userId, fileOrObject); + expect(result).toBe("sensitive_update:789, file.txt"); + }); +}); +describe("sensitive_delete", () => { + it("should return the correct string", () => { + const userId = "123"; + const fileOrObject = "file.txt"; + const result = sensitive_delete(userId, fileOrObject); + expect(result).toBe("sensitive_delete:123, file.txt"); + }); + + it("should handle numeric userId", () => { + const userId = 456; + const fileOrObject = "object"; + const result = sensitive_delete(userId, fileOrObject); + expect(result).toBe("sensitive_delete:456, object"); + }); + + it("should handle bigint userId", () => { + const userId = BigInt(789); + const fileOrObject = "file.txt"; + const result = sensitive_delete(userId, fileOrObject); + expect(result).toBe("sensitive_delete:789, file.txt"); + }); +}); diff --git a/src/vocab/sensitive-data-changes.ts b/src/vocab/sensitive-data-changes.ts new file mode 100644 index 0000000..eaa2012 --- /dev/null +++ b/src/vocab/sensitive-data-changes.ts @@ -0,0 +1,132 @@ +/** + * When a new piece of data is created and marked as sensitive or placed into a directory/table/repository where sensitive data is stored, + * that creation should be logged and reviewed periodically. + * + * **Level:** `WARN` + * + * @example + * ```ts + * const userId = "joebob1"; + * const fileOrObject = "/users/admin/some/important/path"; + * logger.warn({ event: sensitive_create(userId, fileOrObject) }, `User ${userId} created a new file in ${fileOrObject}`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "sensitive_create:joebob1, /users/admin/some/important/path", + * "level": "WARN", + * "description": "User joebob1 created a new file in /users/admin/some/important/path", + * "requestId": "cd1f3327-48ea-4614-bce0-260d14e30e93" + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - sensitive_create](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#sensitive_createuseridfileobject) + */ +export function sensitive_create< + U extends string | number | bigint, + F extends string, +>(userId: U, fileOrObject: F) { + // The space is intentional, based on the OWASP example. + return `sensitive_create:${userId}, ${fileOrObject}` as const; +} + +/** + * All data marked as sensitive or placed into a directory/table/repository where sensitive data + * is stored should be have access logged and reviewed periodically. + * + * **Level:** `WARN` + * + * @example + * ```ts + * const userId = "joebob1"; + * const fileOrObject = "/users/admin/some/important/path"; + * logger.info({ event: sensitive_read(userId, fileOrObject) }, `User ${userId} read file ${fileOrObject}`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "sensitive_read:joebob1, /users/admin/some/important/path", + * "level": "INFO", + * "description": "User joebob1 read file /users/admin/some/important/path", + * "requestId": "df064296-507f-46e3-8384-bb14966cd2bc" + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - sensitive_read](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#sensitive_readuseridfileobject) + */ +export function sensitive_read< + U extends string | number | bigint, + F extends string, +>(userId: U, fileOrObject: F) { + // The space is intentional, based on the OWASP example. + return `sensitive_read:${userId}, ${fileOrObject}` as const; +} + +/** +/** + * All data marked as sensitive or placed into a directory/table/repository where sensitive data is stored + * should be have updates to the data logged and reviewed periodically. + * + * **Level:** `WARN` + * + * @example + * ```ts + * const userId = "joebob1"; + * const fileOrObject = "/users/admin/some/important/path"; + * logger.warn({ event: sensitive_update(userId, fileOrObject) }, `User ${userId} modified file ${fileOrObject}`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "sensitive_update:joebob1, /users/admin/some/important/path", + * "level": "WARN", + * "description": "User joebob1 modified file /users/admin/some/important/path", + * "requestId": "9345a8a2-611e-4053-a75c-05c29101dc59" + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - sensitive_update](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#sensitive_updateuseridfileobject) + */ +export function sensitive_update< + U extends string | number | bigint, + F extends string, +>(userId: U, fileOrObject: F) { + return `sensitive_update:${userId}, ${fileOrObject}` as const; +} + +/** + * All data marked as sensitive or placed into a directory/table/repository where sensitive data is stored should have deletions + * of the data logged and reviewed periodically. + * The file should not be immediately deleted but marked for deletion and an archive of the file should be maintained according + * to legal/privacy requirements. + * + * **Level:** `WARN` + * + * @example + * ```ts + * const userId = "joebob1"; + * const fileOrObject = "/users/admin/some/important/path"; + * logger.warn({ event: sensitive_delete(userId, fileOrObject) }, `User ${userId} marked file ${fileOrObject} for deletion`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "sensitive_delete:joebob1, /users/admin/some/important/path", + * "level": "WARN", + * "description": "User joebob1 marked file /users/admin/some/important/path for deletion", + * "requestId": "f6a08db8-df7a-4b13-82ee-18aaf15c7fc1" + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - sensitive_delete](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#sensitive_deleteuseridfileobject) + */ +export function sensitive_delete< + U extends string | number | bigint, + F extends string, +>(userId: U, fileOrObject: F) { + return `sensitive_delete:${userId}, ${fileOrObject}` as const; +} diff --git a/src/vocab/sequence-errors.test.ts b/src/vocab/sequence-errors.test.ts new file mode 100644 index 0000000..872bf1a --- /dev/null +++ b/src/vocab/sequence-errors.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "bun:test"; +import { sequence_fail } from "./sequence-errors.js"; + +describe("sequence_fail", () => { + it("should return the correct string", () => { + const result = sequence_fail("user123"); + expect(result).toBe("sequence_fail:user123"); + }); + + it("should return the correct string for number input", () => { + const result = sequence_fail(123); + expect(result).toBe("sequence_fail:123"); + }); + + it("should return the correct string for bigint input", () => { + const result = sequence_fail(BigInt(456)); + expect(result).toBe("sequence_fail:456"); + }); +}); diff --git a/src/vocab/sequence-errors.ts b/src/vocab/sequence-errors.ts new file mode 100644 index 0000000..ff08c77 --- /dev/null +++ b/src/vocab/sequence-errors.ts @@ -0,0 +1,26 @@ +/** + * When a user reaches a part of the application out of sequence it may indicate intentional abuse of the business logic and should be tracked. + * + * **Level:** `WARN` + * + * @example + * ```ts + * const userId = "joebob1"; + * logger.warn({ event: sequence_fail(userId) }, `User ${userId} has reached a part of the application out of the normal application flow.`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "sequence_fail:joebob1", + * "level": "WARN", + * "description": "User joebob1 has reached a part of the application out of the normal application flow.", + * "requestId": "b6de2047-08f0-4041-aab1-c693d337d63b" + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - sequence_fail](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#sequence_failuserid) + */ +export function sequence_fail(userId: U) { + return `sequence_fail:${userId}` as const; +} diff --git a/src/vocab/session-management.test.ts b/src/vocab/session-management.test.ts new file mode 100644 index 0000000..5e2c37e --- /dev/null +++ b/src/vocab/session-management.test.ts @@ -0,0 +1,105 @@ +import { describe, expect, it } from "bun:test"; +import { + session_created, + session_expired, + session_renewed, + session_use_after_expire, +} from "./session-management.js"; + +describe("session_created", () => { + it("should return the correct string", () => { + const result = session_created("user123"); + expect(result).toBe("session_created:user123"); + }); + + it("should return the correct string with a number", () => { + const result = session_created(123); + expect(result).toBe("session_created:123"); + }); + + it("should return the correct string with a bigint", () => { + const result = session_created(BigInt(456)); + expect(result).toBe("session_created:456"); + }); +}); + +describe("session_renewed", () => { + it("should return the correct string", () => { + const userId = "123"; + const result = session_renewed(userId); + expect(result).toBe("session_renewed:123"); + }); + + it("should return the correct string for number", () => { + const userId = 456; + const result = session_renewed(userId); + expect(result).toBe("session_renewed:456"); + }); + + it("should return the correct string for bigint", () => { + const userId = BigInt(789); + const result = session_renewed(userId); + expect(result).toBe("session_renewed:789"); + }); +}); + +describe("session_expired", () => { + it("should return the correct session expired message", () => { + const userId = 123; + const reason = "inactive"; + const expected = "session_expired:123,inactive"; + + const result = session_expired(userId, reason); + + expect(result).toBe(expected); + }); + + it("should handle string userId and reason", () => { + const userId = "456"; + const reason = "expired"; + const expected = "session_expired:456,expired"; + + const result = session_expired(userId, reason); + + expect(result).toBe(expected); + }); + + it("should handle bigint userId", () => { + const userId = BigInt(789); + const reason = "unknown"; + const expected = "session_expired:789,unknown"; + + const result = session_expired(userId, reason); + + expect(result).toBe(expected); + }); +}); + +describe("session_use_after_expire", () => { + it("should return the correct session string", () => { + const userId = "123"; + const expected = "session_use_after_expire:123"; + + const result = session_use_after_expire(userId); + + expect(result).toEqual(expected); + }); + + it("should return the correct session string for numeric userId", () => { + const userId = 456; + const expected = "session_use_after_expire:456"; + + const result = session_use_after_expire(userId); + + expect(result).toEqual(expected); + }); + + it("should return the correct session string for bigint userId", () => { + const userId = BigInt(789); + const expected = "session_use_after_expire:789"; + + const result = session_use_after_expire(userId); + + expect(result).toEqual(expected); + }); +}); diff --git a/src/vocab/session-management.ts b/src/vocab/session-management.ts new file mode 100644 index 0000000..fc24ddb --- /dev/null +++ b/src/vocab/session-management.ts @@ -0,0 +1,119 @@ +/** + * When a new authenticated session is created that session may be logged and activity monitored. + * + * **Level:** `INFO` + * + * @example + * ```ts + * const userId = "joebob1"; + * logger.info({ event: session_created(userId) }, `User ${userId} has started a new session`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "session_created:joebob1", + * "level": "INFO", + * "description": "User joebob1 has started a new session", + * "requestId": "3852a457-6d69-4f8c-a98e-aeafe5f1fdd1" + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - session_created](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#session_createduserid) + */ +export function session_created(userId: U) { + return `session_created:${userId}` as const; +} + +/** + * When a user is warned of session to be expired/revoked and chooses to extend their session that activity should be logged. + * Also, if the system in question contains highly confidential data then extending a session may require additional verification. + * + * **Level:** `INFO` + * + * @example + * ```ts + * const userId = "joebob1"; + * logger.info({ event: session_renewed(userId) }, `User ${userId} was warned of expiring session and extended`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "session_renewed:joebob1", + * "level": "INFO", + * "description": "User joebob1 was warned of expiring session and extended", + * "requestId": "ec67b394-8391-4579-94fb-a36719ab5292" + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - session_renewed](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#session_reneweduserid) + */ +export function session_renewed(userId: U) { + return `session_renewed:${userId}` as const; +} + +/** + * When a session expires, especially in the case of an authenticated session or with sensitive data, + * then that session expiry may be logged and clarifying data included. + * The reason code may be any such as: logout, timeout, revoked, etc. + * Sessions should never be deleted but rather expired in the case of revocation requirement. + * + * **Level:** `INFO` + * + * @example + * ```ts + * const userId = "joebob1"; + * const reason = "revoked"; + * logger.info({ event: session_expired(userId, reason) }, `User ${userId} session expired due to administrator ${reason}.`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "session_expired:joebob1,revoked", + * "level": "INFO", + * "description": "User joebob1 session expired due to administrator revocation.", + * "requestId": "5c0f4762-1170-437f-b618-4406847a533b" + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - session_expired](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#session_expireduseridreason) + */ +export function session_expired< + U extends string | number | bigint, + R extends string, +>(userId: U, reason: R) { + return `session_expired:${userId},${reason}` as const; +} + +/** + * In the case a user attempts to access systems with an expired session, it may be helpful to log, + * especially if combined with subsequent login failure. + * This could identify a case where a malicious user is attempting a session hijack or directly accessing another person's machine/browser. + * + * **Level:** `WARN` + * + * @example + * ```ts + * const userId = "joebob1"; + * logger.warn({ event: session_use_after_expire(userId) }, `User ${userId} attempted access after session expired.`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "session_use_after_expire:joebob1", + * "level": "WARN", + * "description": "User joebob1 attempted access after session expired.", + * "requestId": "..." + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - session_use_after_expire](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#session_use_after_expireuserid) + */ +export function session_use_after_expire( + userId: U, +) { + return `session_use_after_expire:${userId}` as const; +} diff --git a/src/vocab/system-events.test.ts b/src/vocab/system-events.test.ts new file mode 100644 index 0000000..1afe8cb --- /dev/null +++ b/src/vocab/system-events.test.ts @@ -0,0 +1,142 @@ +import { describe, expect, it } from "bun:test"; +import { + sys_crash, + sys_monitor_disabled, + sys_monitor_enabled, + sys_restarted, + sys_shutdown, + sys_startup, +} from "./system-events.js"; + +describe("sys_startup", () => { + it("should return the correct string when userId is a string", () => { + const result = sys_startup("user123"); + expect(result).toBe("sys_startup:user123"); + }); + + it("should return the correct string when userId is a number", () => { + const result = sys_startup(123); + expect(result).toBe("sys_startup:123"); + }); + + it("should return the correct string when userId is a bigint", () => { + const result = sys_startup(BigInt(123)); + expect(result).toBe("sys_startup:123"); + }); +}); + +describe("sys_shutdown", () => { + it("should return the correct system event string", () => { + const userId = "123"; + const expected = "sys_shutdown:123"; + const result = sys_shutdown(userId); + expect(result).toEqual(expected); + }); + + it("should return the correct system event string with a number", () => { + const userId = 456; + const expected = "sys_shutdown:456"; + const result = sys_shutdown(userId); + expect(result).toEqual(expected); + }); + + it("should return the correct system event string with a bigint", () => { + const userId = BigInt(789); + const expected = "sys_shutdown:789"; + const result = sys_shutdown(userId); + expect(result).toEqual(expected); + }); +}); + +describe("sys_restarted", () => { + it("should return the correct system restart event string", () => { + const userId = "123"; + const expected = "sys_restart:123"; + const result = sys_restarted(userId); + expect(result).toEqual(expected); + }); + + it("should return the correct system restart event string with a number", () => { + const userId = 456; + const expected = "sys_restart:456"; + const result = sys_restarted(userId); + expect(result).toEqual(expected); + }); + + it("should return the correct system restart event string with a bigint", () => { + const userId = BigInt(789); + const expected = "sys_restart:789"; + const result = sys_restarted(userId); + expect(result).toEqual(expected); + }); +}); + +describe("sys_crash", () => { + it("should return the correct system crash event", () => { + const reason = "out_of_memory"; + const result = sys_crash(reason); + const expected = "sys_crash:out_of_memory"; + expect(result).toEqual(expected); + }); +}); + +describe("sys_monitor_disabled", () => { + it("should return the correct event string", () => { + const userId = "123"; + const agent = "test-agent"; + const expectedEvent = "sys_monitor_disabled:123,test-agent"; + + const result = sys_monitor_disabled(userId, agent); + + expect(result).toBe(expectedEvent); + }); + + it("should handle numeric userId", () => { + const userId = 456; + const agent = "test-agent"; + const expectedEvent = "sys_monitor_disabled:456,test-agent"; + + const result = sys_monitor_disabled(userId, agent); + + expect(result).toBe(expectedEvent); + }); + + it("should handle bigint userId", () => { + const userId = BigInt(789); + const agent = "test-agent"; + const expectedEvent = "sys_monitor_disabled:789,test-agent"; + + const result = sys_monitor_disabled(userId, agent); + + expect(result).toBe(expectedEvent); + }); +}); + +describe("sys_monitor_enabled", () => { + it("should return the correct system event string", () => { + const userId = "123"; + const agent = "web"; + + const result = sys_monitor_enabled(userId, agent); + + expect(result).toBe("sys_monitor_enabled:123,web"); + }); + + it("should handle numeric userId", () => { + const userId = 456; + const agent = "mobile"; + + const result = sys_monitor_enabled(userId, agent); + + expect(result).toBe("sys_monitor_enabled:456,mobile"); + }); + + it("should handle bigint userId", () => { + const userId = BigInt(789); + const agent = "desktop"; + + const result = sys_monitor_enabled(userId, agent); + + expect(result).toBe("sys_monitor_enabled:789,desktop"); + }); +}); diff --git a/src/vocab/system-events.ts b/src/vocab/system-events.ts new file mode 100644 index 0000000..a6c010d --- /dev/null +++ b/src/vocab/system-events.ts @@ -0,0 +1,179 @@ +/** + * When a system is first started it can be valuable to log the startup, even if the system is serverless or a container, + * especially if possible to log the user that initiated the system. + * + * **Level:** `WARN` + * + * @example + * ```ts + * const userId = "joebob1"; + * logger.warn({ event: sys_startup(userId) }, `User ${userId} spawned a new instance`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "sys_startup:joebob1", + * "level": "WARN", + * "description": "User joebob1 spawned a new instance", + * "requestId": "27939458-b680-4132-925f-4dc38678b4b2" + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - sys_startup](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#sys_shutdownuserid) + */ +export function sys_startup(userId: U) { + return `sys_startup:${userId}` as const; +} + +/** + * When a system is shut down it can be valuable to log the event, even if the system is serverless or a container, + * especially if possible to log the user that initiated the system. + * + * **Level:** `WARN` + * + * @example + * ```ts + * const userId = "joebob1"; + * logger.warn({ event: sys_shutdown(userId) }, `User ${userId} stopped this instance`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "sys_shutdown:joebob1", + * "level": "WARN", + * "description": "User joebob1 stopped this instance", + * "requestId": "cbe8f317-085c-46a6-9551-057b7c275b63" + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - sys_shutdown](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#sys_shutdownuserid) + */ +export function sys_shutdown(userId: U) { + return `sys_shutdown:${userId}` as const; +} + +/** + * When a system is restarted it can be valuable to log the event, even if the system is serverless or a container, + * especially if possible to log the user that initiated the system. + * + * **Level:** `WARN` + * + * @example + * ```ts + * const userId = "joebob1"; + * logger.warn({ event: sys_restarted(userId) }, `User ${userId} initiated a restart`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "sys_restart:joebob1", + * "level": "WARN", + * "description": "User joebob1 initiated a restart", + * "requestId": "1e7b4e37-7acd-4d08-89e9-8a9b07de6a90" + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - sys_restarted](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#sys_restartuserid) + */ +export function sys_restarted(userId: U) { + return `sys_restart:${userId}` as const; +} + +/** + * If possible to catch an unstable condition resulting in the crash of a system, logging that event could be helpful, + * especially if the event is triggered by an attack. + * + * **Level:** `WARN` + * + * @param reason The reason for the system crash. + * @example + * ```ts + * const reason = "outofmemory"; + * logger.warn({ event: sys_crash(reason) }, `The system crashed due to ${reason} error.`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "sys_crash:outofmemory", + * "level": "WARN", + * "description": "The system crashed due to Out of Memory error.", + * "requestId": "43549e84-4d5a-402c-a5cd-753a28795f82" + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - sys_crash](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#sys_crashreason) + */ +export function sys_crash(reason: R) { + return `sys_crash:${reason}` as const; +} + +/** + * If your systems contain agents responsible for file integrity, resources, logging, virus, etc. it is especially valuable to know if they are halted and by whom. + * + * **Level:** `WARN` + * + * @param userId The user who disabled the system monitor. + * @param agent The name of the agent that was disabled. + * @example + * ```ts + * const userId = "joebob1"; + * const agent = "crowdstrike"; + * logger.warn({ event: sys_monitor_disabled(userId, agent) }, `User ${userId} has disabled ${agent}`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "sys_monitor_disabled:joebob1,crowdstrike", + * "level": "WARN", + * "description": "User joebob1 has disabled CrowdStrike", + * "requestId": "897365e2-7cd2-475d-a94b-082bf11f368e" + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - sys_monitor_disabled](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#sys_monitor_disableduseridmonitor) + */ +export function sys_monitor_disabled< + U extends string | number | bigint, + A extends string, +>(userId: U, agent: A) { + return `sys_monitor_disabled:${userId},${agent}` as const; +} + +/** + * If your systems contain agents responsible for file integrity, resources, logging, virus, etc. + * it is especially valuable to know if they are started again after being stopped, and by whom. + * + * **Level:** `WARN` + * + * @param userId The user who enabled the system monitor. + * @param agent The name of the agent that was enabled. + * @example + * ```ts + * const userId = "joebob1"; + * const agent = "crowdstrike"; + * logger.warn({ event: sys_monitor_enabled(userId, agent) }, `User ${userId} has enabled ${agent}`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "sys_monitor_enabled:joebob1,crowdstrike", + * "level": "WARN", + * "description": "User joebob1 has enabled CrowdStrike", + * "requestId": "897365e2-7cd2-475d-a94b-082bf11f368e" + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - sys_monitor_enabled](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#sys_monitor_enableduseridmonitor) + */ +export function sys_monitor_enabled< + U extends string | number | bigint, + A extends string, +>(userId: U, agent: A) { + return `sys_monitor_enabled:${userId},${agent}` as const; +} diff --git a/src/vocab/user-management.test.ts b/src/vocab/user-management.test.ts new file mode 100644 index 0000000..2ec1a82 --- /dev/null +++ b/src/vocab/user-management.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from "bun:test"; +import { + user_archived, + user_created, + user_deleted, + user_updated, +} from "./user-management.js"; + +describe("user_created", () => { + it("should return the correct string when userId, newUserId, and role are strings", () => { + const result = user_created("user123", "newUser456", "admin"); + expect(result).toBe("user_created:user123,newUser456,admin"); + }); + + it("should return the correct string when userId, newUserId, and role are numbers, and we pass attributes of length 0", () => { + const result = user_created(123, 456, "user", [] as const); + expect(result).toBe("user_created:123,456,user:"); + }); + + it("should return the correct string when we pass attributes of length 1", () => { + const result = user_created(123, "newUser456", "user", [ + "attribute1", + ] as const); + expect(result).toBe("user_created:123,newUser456,user:attribute1"); + }); + + it("should return the correct string when we pass attributes of length 2", () => { + const result = user_created(123, "newUser456", "user", [ + "attribute1", + "attribute2", + ] as const); + expect(result).toBe( + "user_created:123,newUser456,user:attribute1,attribute2", + ); + }); +}); + +describe("user_updated", () => { + it("should return the correct string when userId, newUserId, and role are strings", () => { + const result = user_updated("user123", "newUser456", "admin"); + expect(result).toBe("user_updated:user123,newUser456,admin"); + }); + it("should return the correct string when userId, newUserId, and role are numbers, and we pass attributes of length 0", () => { + const result = user_updated(123, 456, "user", [] as const); + expect(result).toBe("user_updated:123,456,user:"); + }); + it("should return the correct string when we pass attributes of length 1", () => { + const result = user_updated(123, "newUser456", "user", [ + "attribute1", + ] as const); + expect(result).toBe("user_updated:123,newUser456,user:attribute1"); + }); + it("should return the correct string when we pass attributes of length 2", () => { + const result = user_updated(123, "newUser456", "user", [ + "attribute1", + "attribute2", + ] as const); + expect(result).toBe( + "user_updated:123,newUser456,user:attribute1,attribute2", + ); + }); +}); + +describe("user_archived", () => { + it("should return the correct string", () => { + const userId = 123; + const archivedUserId = "456"; + const result = user_archived(userId, archivedUserId); + const expected = "user_archived:123,456"; + expect(result).toBe(expected); + }); + + it("should handle different types of userId and archivedUserId", () => { + const userId = "abc"; + const archivedUserId = 789n; + const result = user_archived(userId, archivedUserId); + const expected = "user_archived:abc,789"; + expect(result).toBe(expected); + }); +}); + +describe("user_deleted", () => { + it("should return the correct string", () => { + const userId = 123; + const deletedUserId = "456"; + const result = user_deleted(userId, deletedUserId); + expect(result).toBe("user_deleted:123,456"); + }); + + it("should return the correct string with bigint values", () => { + const userId = BigInt(123); + const deletedUserId = BigInt(456); + const result = user_deleted(userId, deletedUserId); + expect(result).toBe("user_deleted:123,456"); + }); +}); diff --git a/src/vocab/user-management.ts b/src/vocab/user-management.ts new file mode 100644 index 0000000..cecc7eb --- /dev/null +++ b/src/vocab/user-management.ts @@ -0,0 +1,181 @@ +import type { Join } from "type-fest"; + +/** + * When creating new users, logging the specifics of the user creation event is helpful, + * especially if new users can be created with administration privileges. + * + * **Level:** `WARN` + * + * @example + * ```ts + * const userId = "joebob1"; + * const newUserId = "user1"; + * const role = "admin"; + * const attributes = ["create", "update", "delete"]; + * logger.warn({ event: user_created(userId, newUserId, role, attributes) }, `User ${userId} created ${newUserId} with ${role}:${attributes.join(",")} privilege attributes`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "user_created:joebob1,user1,admin:create,update,delete", + * "level": "WARN", + * "description": "User joebob1 created user1 with admin:create,update,delete privilege attributes", + * "requestId": "58705027-fc55-49b3-b8df-ee4842ef105a" + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - user_created](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#user_createduseridnewuseridattributesonetwothree) + */ +export function user_created< + U extends string | number | bigint, + N extends string | number | bigint, + R extends string, +>(userId: U, newUserId: N, role: R): `user_created:${U},${N},${R}`; +export function user_created< + U extends string | number | bigint, + N extends string | number | bigint, + R extends string, + A extends readonly string[], +>( + userId: U, + newUserId: N, + role: R, + attributes: A, +): `user_created:${U},${N},${R}:${Join}`; +export function user_created< + U extends string | number | bigint, + N extends string | number | bigint, + R extends string, + A extends readonly string[], +>(userId: U, newUserId: N, role: R, attributes?: A) { + if (attributes === undefined) { + return `user_created:${userId},${newUserId},${role}` as const; + } + return `user_created:${userId},${newUserId},${role}:${ + attributes.join(",") as Join + }` as const; +} + +/** + * When updating users, logging the specifics of the user update event is helpful, + * especially if users can be updated with administration privileges. + * + * **Level:** `WARN` + * + * @example + * ```ts + * const userId = "joebob1"; + * const newUserId = "user1"; + * const role = "admin"; + * const attributes = ["create", "update", "delete"]; + * logger.warn({ event: user_updated(userId, newUserId, role, attributes) }, `User ${userId} updated ${newUserId} with ${role}:${attributes.join(",")} privilege attributes`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "user_updated:joebob1,user1,admin:create,update,delete", + * "level": "WARN", + * "description": "User joebob1 updated user1 with admin:create,update,delete privilege attributes", + * "requestId": "55acb903-49a8-4d31-ae52-c7053897cba8" + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - user_updated](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#user_updateduseridonuseridattributesonetwothree) + */ +export function user_updated< + U extends string | number | bigint, + N extends string | number | bigint, + R extends string, +>(userId: U, newUserId: N, role: R): `user_updated:${U},${N},${R}`; +export function user_updated< + U extends string | number | bigint, + N extends string | number | bigint, + R extends string, + A extends readonly string[], +>( + userId: U, + newUserId: N, + role: R, + attributes: A, +): `user_updated:${U},${N},${R}:${Join}`; +export function user_updated< + U extends string | number | bigint, + N extends string | number | bigint, + R extends string, + A extends readonly string[], +>(userId: U, newUserId: N, role: R, attributes?: A) { + if (attributes === undefined) { + return `user_updated:${userId},${newUserId},${role}` as const; + } + return `user_updated:${userId},${newUserId},${role}:${ + attributes.join(",") as Join + }` as const; +} + +/** + * It is always best to archive users rather than deleting, except where required. + * When archiving users, logging the specifics of the user archive event is helpful. + * A malicious user could use this feature to deny service to legitimate users. + * + * **Level:** `WARN` + * + * @example + * ```ts + * const userId = "joebob1"; + * const onUserId = "user1"; + * logger.warn({ event: user_archived(userId, onUserId) }, `User ${userId} archived ${onUserId}`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "user_archived:joebob1,user1", + * "level": "WARN", + * "description": "User joebob1 archived user1", + * "requestId": "e5ce13d6-bbf7-4479-be87-b33012a84640" + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - user_archived](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#user_archiveduseridonuserid) + */ +export function user_archived< + U extends string | number | bigint, + N extends string | number | bigint, +>(userId: U, archivedUserId: N) { + return `user_archived:${userId},${archivedUserId}` as const; +} + +/** + * It is always best to archive users rather than deleting, except where required. + * When deleting users, logging the specifics of the user delete event is helpful. + * A malicious user could use this feature to deny service to legitimate users. + * + * **Level:** `WARN` + * + * @example + * ```ts + * const userId = "joebob1"; + * const onUserId = "user1"; + * logger.warn({ event: user_deleted(userId, onUserId) }, `User ${userId} has deleted ${deletedUserId}`); + * ``` + * @example + * ```json + * { + * "datetime": "2019-01-01 00:00:00,000", + * "appid": "foobar.netportal_auth", + * "event": "user_deleted:joebob1,user1", + * "level": "WARN", + * "description": "User joebob1 has deleted user1", + * "requestId": "5868ae61-e94e-4419-b231-0d81479e2abb" + * } + * ``` + * @see [OWASP Logging Vocabulary Cheat Sheet - user_deleted](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Vocabulary_Cheat_Sheet.html#user_deleteduseridonuserid) + */ +export function user_deleted< + U extends string | number | bigint, + N extends string | number | bigint, +>(userId: U, deletedUserId: N) { + return `user_deleted:${userId},${deletedUserId}` as const; +} diff --git a/tsup.config.ts b/tsup.config.ts index c9e227a..899ad1a 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,7 +1,9 @@ import { defineConfig } from "tsup"; export default defineConfig({ - entry: ["src/index.ts"], + entry: { + vocab: "./src/vocab/index.ts", + }, splitting: false, sourcemap: true, format: ["cjs", "esm"],