From 89e34dea170a467e26bba688e8b7ec9894e67f4f Mon Sep 17 00:00:00 2001 From: blessme247 Date: Tue, 23 Jun 2026 04:55:35 +0100 Subject: [PATCH 1/2] feat(audit): implement comprehensive audit log search, filtering and compliance export --- .vscode/settings.json | 3 + package-lock.json | 126 +++++-------- package.json | 1 + .../algorithms/export-signing.service.ts | 25 +++ .../audit/audit-log.controller.ts | 85 +++++++++ src/infrastructure/audit/audit-log.module.ts | 15 ++ src/infrastructure/audit/audit-log.service.ts | 172 ++++++++++++++++-- .../audit/dto/audit-log-response.dto.ts | 54 ++++++ .../audit/dto/query-audit-log.dto.ts | 84 +++++++++ .../audit/entities/audit-log.entity.ts | 112 ++++++++++++ .../audit/guards/compliance-officer.guard.ts | 29 +++ .../audit/migration/create-audit-log.ts | 52 ++++++ 12 files changed, 662 insertions(+), 96 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/infrastructure/audit/algorithms/export-signing.service.ts create mode 100644 src/infrastructure/audit/audit-log.controller.ts create mode 100644 src/infrastructure/audit/audit-log.module.ts create mode 100644 src/infrastructure/audit/dto/audit-log-response.dto.ts create mode 100644 src/infrastructure/audit/dto/query-audit-log.dto.ts create mode 100644 src/infrastructure/audit/entities/audit-log.entity.ts create mode 100644 src/infrastructure/audit/guards/compliance-officer.guard.ts create mode 100644 src/infrastructure/audit/migration/create-audit-log.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..edc55ce --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "snyk.advanced.autoSelectOrganization": true +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7776472..ce4c221 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.3.0", "@nestjs/platform-socket.io": "^10.4.22", + "@nestjs/schedule": "^6.1.3", "@nestjs/swagger": "^7.4.2", "@nestjs/terminus": "^11.1.1", "@nestjs/throttler": "^6.5.0", @@ -1030,7 +1031,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", - "dev": true, "license": "MIT", "optional": true }, @@ -2620,6 +2620,19 @@ "node": ">=10.2.0" } }, + "node_modules/@nestjs/schedule": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.1.3.tgz", + "integrity": "sha512-RflMFOpR16Dwd1jAUbeB4mfGTCh65fvEdL4mSjQPJChpkRGRjIXjb+6YQcK2faQrVT60c9DmLmoVR7/ONCtuYQ==", + "license": "MIT", + "dependencies": { + "cron": "4.4.0" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0" + } + }, "node_modules/@nestjs/schematics": { "version": "10.2.3", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.2.3.tgz", @@ -2911,7 +2924,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -2924,7 +2936,6 @@ "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", "deprecated": "This functionality has been moved to @npmcli/fs", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2939,7 +2950,6 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2952,7 +2962,6 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -2974,7 +2983,6 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -2989,7 +2997,6 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -6392,7 +6399,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -6676,6 +6682,12 @@ "@types/node": "*" } }, + "node_modules/@types/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-gW+Oib+vUtGJBtNC8V9Reww0oIpusw+4m81uncg9REGZAJfqOQHfo/nkabnc7w0QReXyPqjrbWMJk6NuAkiX3Q==", + "license": "MIT" + }, "node_modules/@types/memcached": { "version": "2.2.10", "resolved": "https://registry.npmjs.org/@types/memcached/-/memcached-2.2.10.tgz", @@ -7356,7 +7368,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true, "license": "ISC", "optional": true }, @@ -7453,7 +7464,6 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -7467,7 +7477,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -7659,7 +7668,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", - "dev": true, "license": "ISC", "optional": true }, @@ -7668,7 +7676,6 @@ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", "deprecated": "This package is no longer supported.", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -8392,7 +8399,6 @@ "version": "15.3.0", "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -8423,7 +8429,6 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -8436,7 +8441,6 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -8458,7 +8462,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -8472,7 +8475,6 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -8486,7 +8488,6 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -8501,7 +8502,6 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -8518,7 +8518,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, "license": "ISC", "optional": true }, @@ -8751,7 +8750,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -8895,7 +8893,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "dev": true, "license": "ISC", "optional": true, "bin": { @@ -8962,7 +8959,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/concat-stream": { @@ -8990,7 +8987,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "dev": true, "license": "ISC", "optional": true }, @@ -9124,6 +9120,23 @@ "devOptional": true, "license": "MIT" }, + "node_modules/cron": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/cron/-/cron-4.4.0.tgz", + "integrity": "sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ==", + "license": "MIT", + "dependencies": { + "@types/luxon": "~3.7.0", + "luxon": "~3.7.0" + }, + "engines": { + "node": ">=18.x" + }, + "funding": { + "type": "ko-fi", + "url": "https://ko-fi.com/intcreator" + } + }, "node_modules/cron-parser": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", @@ -9371,7 +9384,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "dev": true, "license": "MIT", "optional": true }, @@ -9715,7 +9727,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -9726,7 +9737,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, "license": "MIT", "optional": true }, @@ -11304,7 +11314,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/fsevents": { @@ -11370,7 +11380,6 @@ "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", "deprecated": "This package is no longer supported.", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -11391,7 +11400,6 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, "license": "ISC", "optional": true }, @@ -11708,7 +11716,7 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/graphemer": { @@ -11841,7 +11849,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "dev": true, "license": "ISC", "optional": true }, @@ -11925,7 +11932,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "dev": true, "license": "BSD-2-Clause", "optional": true }, @@ -11953,7 +11959,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -11992,7 +11997,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -12101,7 +12105,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -12111,7 +12115,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -12122,7 +12125,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "dev": true, "license": "ISC", "optional": true }, @@ -12131,7 +12133,7 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -12509,7 +12511,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", - "dev": true, "license": "MIT", "optional": true }, @@ -13967,7 +13968,6 @@ "version": "9.1.0", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -13996,7 +13996,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -14010,7 +14009,6 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -14024,7 +14022,6 @@ "version": "0.6.4", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -14035,7 +14032,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, "license": "ISC", "optional": true }, @@ -14253,7 +14249,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -14267,7 +14262,6 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -14281,7 +14275,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, "license": "ISC", "optional": true }, @@ -14289,7 +14282,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -14308,7 +14300,6 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -14322,7 +14313,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, "license": "ISC", "optional": true }, @@ -14330,7 +14320,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", - "dev": true, "license": "BlueOak-1.0.0", "optional": true, "dependencies": { @@ -14344,7 +14333,6 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -14358,7 +14346,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, "license": "ISC", "optional": true }, @@ -14366,7 +14353,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -14380,7 +14366,6 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -14394,7 +14379,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, "license": "ISC", "optional": true }, @@ -14402,7 +14386,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -14416,7 +14399,6 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -14430,7 +14412,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, "license": "ISC", "optional": true }, @@ -14679,7 +14660,6 @@ "version": "8.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -14731,7 +14711,6 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -14744,7 +14723,6 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -14766,7 +14744,6 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -14781,7 +14758,6 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -14914,7 +14890,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -14955,7 +14930,6 @@ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", "deprecated": "This package is no longer supported.", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -15277,7 +15251,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -15405,7 +15378,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -15501,7 +15474,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", - "dev": true, "license": "MIT", "optional": true }, @@ -15941,7 +15913,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "dev": true, "license": "ISC", "optional": true }, @@ -15949,7 +15920,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -16565,7 +16535,6 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -17187,7 +17156,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -17299,7 +17267,6 @@ "version": "2.8.9", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -17315,7 +17282,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -17447,7 +17413,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -17461,7 +17426,6 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -17475,7 +17439,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, "license": "ISC", "optional": true }, @@ -18943,7 +18906,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -18954,7 +18916,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -19382,7 +19343,6 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dev": true, "license": "ISC", "optional": true, "dependencies": { diff --git a/package.json b/package.json index 3f1e726..db6964d 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.3.0", "@nestjs/platform-socket.io": "^10.4.22", + "@nestjs/schedule": "^6.1.3", "@nestjs/swagger": "^7.4.2", "@nestjs/terminus": "^11.1.1", "@nestjs/throttler": "^6.5.0", diff --git a/src/infrastructure/audit/algorithms/export-signing.service.ts b/src/infrastructure/audit/algorithms/export-signing.service.ts new file mode 100644 index 0000000..e130573 --- /dev/null +++ b/src/infrastructure/audit/algorithms/export-signing.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from "@nestjs/common"; +import * as crypto from "crypto"; + +@Injectable() +export class ExportSigningService { + private readonly systemKey = process.env.AUDIT_EXPORT_SIGNING_KEY || ""; + + sign(payload: string): string { + if (!this.systemKey) { + throw new Error("AUDIT_EXPORT_SIGNING_KEY is not configured"); + } + return crypto + .createHmac("sha256", this.systemKey) + .update(payload) + .digest("hex"); + } + + verify(payload: string, signature: string): boolean { + const expected = this.sign(payload); + return crypto.timingSafeEqual( + Buffer.from(expected, "hex"), + Buffer.from(signature, "hex"), + ); + } +} \ No newline at end of file diff --git a/src/infrastructure/audit/audit-log.controller.ts b/src/infrastructure/audit/audit-log.controller.ts new file mode 100644 index 0000000..0372213 --- /dev/null +++ b/src/infrastructure/audit/audit-log.controller.ts @@ -0,0 +1,85 @@ +import { + Controller, + Get, + Param, + Query, + UseGuards, + Res, + ParseUUIDPipe, +} from "@nestjs/common"; +import { Response } from "express"; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiParam, + ApiBearerAuth, +} from "@nestjs/swagger"; +import { AuditLogService } from "./audit-log.service"; +import { QueryAuditLogDto, ExportAuditLogDto } from "./dto/query-audit-log.dto"; +import { + AuditLogResponseDto, + AuditLogListResponseDto, +} from "./dto/audit-log-response.dto"; +import { JwtAuthGuard } from "src/core/auth/jwt.guard"; +import { ComplianceOfficerGuard } from "./guards/compliance-officer.guard"; + +@ApiTags("Audit Logs") +@ApiBearerAuth() +@UseGuards(JwtAuthGuard, ComplianceOfficerGuard) +@Controller("audit-logs") +export class AuditLogController { + constructor(private readonly auditLogService: AuditLogService) {} + + @Get() + @ApiOperation({ + summary: "Search and filter audit logs", + description: + "Full-text search with filters by user, action, IP, and date range. Paginated, 100 records per page max.", + }) + @ApiResponse({ status: 200, type: AuditLogListResponseDto }) + @ApiResponse({ status: 403, description: "Forbidden" }) + async query(@Query() query: QueryAuditLogDto): Promise { + return this.auditLogService.query(query); + } + + @Get(":id") + @ApiOperation({ summary: "Get audit log by ID" }) + @ApiParam({ name: "id", type: "string" }) + @ApiResponse({ status: 200, type: AuditLogResponseDto }) + @ApiResponse({ status: 404, description: "Audit log not found" }) + async getById( + @Param("id", ParseUUIDPipe) id: string, + ): Promise { + return this.auditLogService.findById(id); + } + + @Get("export") + @ApiOperation({ + summary: "Bulk export audit logs", + description: + "Exports up to 100,000 records for a date range as signed JSON or CSV.", + }) + @ApiResponse({ status: 200, description: "Export with integrity signature" }) + async export( + @Query() query: ExportAuditLogDto, + @Res() res: Response, + ): Promise { + const { payload, signature } = + query.format === "csv" + ? await this.auditLogService.exportToCsv(query) + : await this.auditLogService.exportToJson(query); + + const ext = query.format === "csv" ? "csv" : "json"; + res.setHeader( + "Content-Type", + query.format === "csv" ? "text/csv" : "application/json", + ); + res.setHeader( + "Content-Disposition", + `attachment; filename="audit-logs-${Date.now()}.${ext}"`, + ); + res.setHeader("X-Signature", signature); + res.send(payload); + } +} \ No newline at end of file diff --git a/src/infrastructure/audit/audit-log.module.ts b/src/infrastructure/audit/audit-log.module.ts new file mode 100644 index 0000000..ab29e60 --- /dev/null +++ b/src/infrastructure/audit/audit-log.module.ts @@ -0,0 +1,15 @@ +import { Module } from "@nestjs/common"; +import { TypeOrmModule } from "@nestjs/typeorm"; +import { ScheduleModule } from "@nestjs/schedule"; +import { AuditLog } from "./entities/audit-log.entity"; +import { AuditLogService } from "./audit-log.service"; +import { AuditLogController } from "./audit-log.controller"; +import { ExportSigningService } from "./algorithms/export-signing.service"; + +@Module({ + imports: [TypeOrmModule.forFeature([AuditLog]), ScheduleModule.forRoot()], + controllers: [AuditLogController], + providers: [AuditLogService, ExportSigningService], + exports: [AuditLogService], +}) +export class AuditLogModule {} \ No newline at end of file diff --git a/src/infrastructure/audit/audit-log.service.ts b/src/infrastructure/audit/audit-log.service.ts index 61d9f3e..1e7d16e 100644 --- a/src/infrastructure/audit/audit-log.service.ts +++ b/src/infrastructure/audit/audit-log.service.ts @@ -1,27 +1,173 @@ -import { Injectable } from "@nestjs/common"; +import { Injectable, NotFoundException } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; +import { Cron, CronExpression } from "@nestjs/schedule"; +import { AuditLog } from "./entities/audit-log.entity"; +import { + QueryAuditLogDto, + ExportAuditLogDto, +} from "./dto/query-audit-log.dto"; +import { AuditLogListResponseDto } from "./dto/audit-log-response.dto"; +import { ExportSigningService } from "./algorithms/export-signing.service"; + +const RETENTION_YEARS = 7; +const ARCHIVE_AFTER_YEARS = 1; @Injectable() export class AuditLogService { - private logs: any[] = []; + constructor( + @InjectRepository(AuditLog) + private readonly repo: Repository, + private readonly signingService: ExportSigningService, + ) {} + + async record(entry: { + userId?: string | null; + action: AuditLog["action"]; + resourceType?: string; + resourceId?: string; + ipAddress: string; + userAgent?: string; + details?: string; + metadata?: Record; + }): Promise { + const searchText = [ + entry.action, + entry.resourceType, + entry.resourceId, + entry.ipAddress, + entry.details, + ] + .filter(Boolean) + .join(" "); + + const log = this.repo.create({ ...entry, searchText }); + return this.repo.save(log); + } + + async query(dto: QueryAuditLogDto): Promise { + const page = dto.page ?? 1; + const limit = dto.limit ?? 100; + + const qb = this.repo.createQueryBuilder("log"); - async recordVerification(result: any) { - const entry = { - type: "VERIFICATION", - ...result, + if (dto.search) { + qb.andWhere( + `to_tsvector('english', log."searchText") @@ websearch_to_tsquery('english', :search)`, + { search: dto.search }, + ); + } + if (dto.userId) qb.andWhere("log.userId = :userId", { userId: dto.userId }); + if (dto.action) qb.andWhere("log.action = :action", { action: dto.action }); + if (dto.ipAddress) + qb.andWhere("log.ipAddress = :ipAddress", { ipAddress: dto.ipAddress }); + if (dto.fromDate) + qb.andWhere("log.createdAt >= :fromDate", { fromDate: dto.fromDate }); + if (dto.toDate) + qb.andWhere("log.createdAt <= :toDate", { toDate: dto.toDate }); + + qb.orderBy("log.createdAt", "DESC") + .skip((page - 1) * limit) + .take(limit); + + const [data, total] = await qb.getManyAndCount(); + + return { + data, + total, + page, + limit, + totalPages: Math.ceil(total / limit), }; + } + + async findById(id: string): Promise { + const log = await this.repo.findOne({ where: { id } }); + if (!log) throw new NotFoundException("Audit log not found"); + return log; + } - this.logs.push(entry); + private async fetchForExport(dto: ExportAuditLogDto): Promise { + return this.repo + .createQueryBuilder("log") + .where("log.createdAt >= :fromDate", { fromDate: dto.fromDate }) + .andWhere("log.createdAt <= :toDate", { toDate: dto.toDate }) + .orderBy("log.createdAt", "ASC") + .take(dto.limit ?? 10000) + .getMany(); + } - // ❗ Immutable simulation (append-only) - Object.freeze(entry); + async exportToJson( + dto: ExportAuditLogDto, + ): Promise<{ payload: string; signature: string }> { + const logs = await this.fetchForExport(dto); + const payload = JSON.stringify(logs); + const signature = this.signingService.sign(payload); + return { payload, signature }; + } - return entry; + async exportToCsv( + dto: ExportAuditLogDto, + ): Promise<{ payload: string; signature: string }> { + const logs = await this.fetchForExport(dto); + const header = [ + "id", + "userId", + "action", + "resourceType", + "resourceId", + "ipAddress", + "createdAt", + ]; + const rows = logs.map((log) => + header + .map((field) => JSON.stringify((log as any)[field] ?? "")) + .join(","), + ); + const payload = [header.join(","), ...rows].join("\n"); + const signature = this.signingService.sign(payload); + return { payload, signature }; } - getLogs(limit = 50) { - return this.logs.slice(-limit); + // Moves logs older than 1 year to cold storage and marks them archived. + // Cold-storage transfer is delegated to an external sink (S3/Glacier); + // this only flips the archivedAt marker once the transfer succeeds. + @Cron(CronExpression.EVERY_DAY_AT_2AM) + async archiveOldLogs(coldStorageWriter?: (logs: AuditLog[]) => Promise) { + const cutoff = new Date(); + cutoff.setFullYear(cutoff.getFullYear() - ARCHIVE_AFTER_YEARS); + + const logs = await this.repo + .createQueryBuilder("log") + .where("log.createdAt < :cutoff", { cutoff }) + .andWhere("log.archivedAt IS NULL") + .getMany(); + + if (logs.length === 0) return; + + if (coldStorageWriter) await coldStorageWriter(logs); + + await this.repo + .createQueryBuilder() + .update(AuditLog) + .set({ archivedAt: new Date() }) + .where("id IN (:...ids)", { ids: logs.map((l) => l.id) }) + .execute(); } -} + // Permanently deletes logs past the 7-year retention period. + @Cron(CronExpression.EVERY_DAY_AT_3AM) + async enforceRetention(): Promise { + const cutoff = new Date(); + cutoff.setFullYear(cutoff.getFullYear() - RETENTION_YEARS); + + await this.repo + .createQueryBuilder() + .delete() + .from(AuditLog) + .where("createdAt < :cutoff", { cutoff }) + .execute(); + } +} diff --git a/src/infrastructure/audit/dto/audit-log-response.dto.ts b/src/infrastructure/audit/dto/audit-log-response.dto.ts new file mode 100644 index 0000000..889820c --- /dev/null +++ b/src/infrastructure/audit/dto/audit-log-response.dto.ts @@ -0,0 +1,54 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { AuditLogAction } from "../entities/audit-log.entity"; + +export class AuditLogResponseDto { + @ApiProperty() + id: string; + + @ApiProperty({ nullable: true }) + userId: string | null; + + @ApiProperty({ enum: AuditLogAction }) + action: AuditLogAction; + + @ApiProperty({ nullable: true }) + resourceType: string | null; + + @ApiProperty({ nullable: true }) + resourceId: string | null; + + @ApiProperty() + ipAddress: string; + + @ApiProperty({ nullable: true }) + userAgent: string | null; + + @ApiProperty({ nullable: true }) + details: string | null; + + @ApiProperty({ nullable: true }) + metadata: Record | null; + + @ApiProperty() + createdAt: Date; + + @ApiProperty({ nullable: true }) + archivedAt: Date | null; +} + +export class AuditLogListResponseDto { + @ApiProperty({ type: [AuditLogResponseDto] }) + data: AuditLogResponseDto[]; + + @ApiProperty() + total: number; + + @ApiProperty() + page: number; + + @ApiProperty() + limit: number; + + @ApiProperty() + totalPages: number; +} \ No newline at end of file diff --git a/src/infrastructure/audit/dto/query-audit-log.dto.ts b/src/infrastructure/audit/dto/query-audit-log.dto.ts new file mode 100644 index 0000000..4a4d736 --- /dev/null +++ b/src/infrastructure/audit/dto/query-audit-log.dto.ts @@ -0,0 +1,84 @@ +import { + IsString, + IsOptional, + IsEnum, + IsUUID, + IsDateString, + IsInt, + IsIP, + Min, + Max, +} from "class-validator"; +import { Type } from "class-transformer"; +import { ApiPropertyOptional } from "@nestjs/swagger"; +import { AuditLogAction } from "../entities/audit-log.entity"; + +export class QueryAuditLogDto { + @ApiPropertyOptional({ description: "Full-text search across log fields" }) + @IsOptional() + @IsString() + search?: string; + + @ApiPropertyOptional({ description: "Filter by user ID" }) + @IsOptional() + @IsUUID() + userId?: string; + + @ApiPropertyOptional({ enum: AuditLogAction }) + @IsOptional() + @IsEnum(AuditLogAction) + action?: AuditLogAction; + + @ApiPropertyOptional({ description: "Filter by IP address" }) + @IsOptional() + @IsIP() + ipAddress?: string; + + @ApiPropertyOptional({ description: "Records created on/after this date" }) + @IsOptional() + @IsDateString() + fromDate?: string; + + @ApiPropertyOptional({ description: "Records created on/before this date" }) + @IsOptional() + @IsDateString() + toDate?: string; + + @ApiPropertyOptional({ default: 1 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page?: number = 1; + + @ApiPropertyOptional({ default: 100, maximum: 100 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + limit?: number = 100; +} + +export class ExportAuditLogDto { + @ApiPropertyOptional({ enum: ["json", "csv"], default: "json" }) + @IsOptional() + @IsString() + format?: "json" | "csv" = "json"; + + @ApiPropertyOptional() + @IsDateString() + fromDate: string; + + @ApiPropertyOptional() + @IsDateString() + toDate: string; + + @ApiPropertyOptional({ default: 10000, maximum: 100000 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100000) + limit?: number = 10000; +} \ No newline at end of file diff --git a/src/infrastructure/audit/entities/audit-log.entity.ts b/src/infrastructure/audit/entities/audit-log.entity.ts new file mode 100644 index 0000000..818e3c9 --- /dev/null +++ b/src/infrastructure/audit/entities/audit-log.entity.ts @@ -0,0 +1,112 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + Index, + ValueTransformer, +} from "typeorm"; +import * as crypto from "crypto"; + +const ENCRYPTION_KEY = Buffer.from( + process.env.AUDIT_LOG_ENCRYPTION_KEY || "", + "hex", +); + +// AES-256-GCM transformer for at-rest encryption of sensitive columns. +// Stored format: base64(iv).base64(authTag).base64(ciphertext) +class EncryptedTransformer implements ValueTransformer { + to(value?: string | null): string | null { + if (value == null) return null; + if (!ENCRYPTION_KEY.length) { + throw new Error("AUDIT_LOG_ENCRYPTION_KEY is not configured"); + } + const iv = crypto.randomBytes(12); + const cipher = crypto.createCipheriv("aes-256-gcm", ENCRYPTION_KEY, iv); + const ciphertext = Buffer.concat([ + cipher.update(value, "utf8"), + cipher.final(), + ]); + const authTag = cipher.getAuthTag(); + return [ + iv.toString("base64"), + authTag.toString("base64"), + ciphertext.toString("base64"), + ].join("."); + } + + from(value?: string | null): string | null { + if (value == null) return null; + const [ivB64, tagB64, dataB64] = value.split("."); + const decipher = crypto.createDecipheriv( + "aes-256-gcm", + ENCRYPTION_KEY, + Buffer.from(ivB64, "base64"), + ); + decipher.setAuthTag(Buffer.from(tagB64, "base64")); + const plaintext = Buffer.concat([ + decipher.update(Buffer.from(dataB64, "base64")), + decipher.final(), + ]); + return plaintext.toString("utf8"); + } +} + +export enum AuditLogAction { + CREATE = "CREATE", + UPDATE = "UPDATE", + DELETE = "DELETE", + LOGIN = "LOGIN", + LOGOUT = "LOGOUT", + ACCESS = "ACCESS", + EXPORT = "EXPORT", + PERMISSION_CHANGE = "PERMISSION_CHANGE", +} + +@Entity("audit_logs") +@Index(["userId", "createdAt"]) +@Index(["action", "createdAt"]) +@Index(["ipAddress", "createdAt"]) +export class AuditLog { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column({ type: "uuid", nullable: true }) + @Index() + userId: string | null; + + @Column({ type: "enum", enum: AuditLogAction }) + @Index() + action: AuditLogAction; + + @Column({ type: "varchar", length: 100, nullable: true }) + resourceType: string | null; + + @Column({ type: "varchar", length: 255, nullable: true }) + resourceId: string | null; + + @Column({ type: "varchar", length: 45 }) + ipAddress: string; + + @Column({ type: "text", nullable: true }) + userAgent: string | null; + + // Encrypted at rest: may contain PII or sensitive request/response context. + @Column({ type: "text", nullable: true, transformer: new EncryptedTransformer() }) + details: string | null; + + @Column({ type: "jsonb", nullable: true }) + metadata: Record | null; + + // Materialized search text, kept in sync by the service layer and indexed + // via a GIN(to_tsvector) index created in the migration. + @Column({ type: "text" }) + searchText: string; + + @CreateDateColumn() + @Index() + createdAt: Date; + + @Column({ type: "timestamptz", nullable: true }) + archivedAt: Date | null; +} \ No newline at end of file diff --git a/src/infrastructure/audit/guards/compliance-officer.guard.ts b/src/infrastructure/audit/guards/compliance-officer.guard.ts new file mode 100644 index 0000000..cf1dec1 --- /dev/null +++ b/src/infrastructure/audit/guards/compliance-officer.guard.ts @@ -0,0 +1,29 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + ForbiddenException, +} from "@nestjs/common"; +import { UserRole } from "src/core/user/entities/user.entity"; + +@Injectable() +export class ComplianceOfficerGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + const roles: string[] = request.user?.roles ?? []; + const user = request.user; + + if (!user) { + throw new ForbiddenException("No authenticated user found"); + } + + // assuming that KYC operators are the compliance officers and admins will also have access + if (user.role !== UserRole.ADMIN || user.role !== UserRole.KYC_OPERATOR) { + throw new ForbiddenException( + "Access to audit logs is restricted to admin and compliance officers", + ); + } + + return true; + } +} \ No newline at end of file diff --git a/src/infrastructure/audit/migration/create-audit-log.ts b/src/infrastructure/audit/migration/create-audit-log.ts new file mode 100644 index 0000000..bc0d2ef --- /dev/null +++ b/src/infrastructure/audit/migration/create-audit-log.ts @@ -0,0 +1,52 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CreateAuditLogTable1700000000000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TYPE "audit_logs_action_enum" AS ENUM ( + 'CREATE', 'UPDATE', 'DELETE', 'LOGIN', 'LOGOUT', 'ACCESS', 'EXPORT', 'PERMISSION_CHANGE' + ); + `); + + await queryRunner.query(` + CREATE TABLE "audit_logs" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid, + "action" "audit_logs_action_enum" NOT NULL, + "resourceType" varchar(100), + "resourceId" varchar(255), + "ipAddress" varchar(45) NOT NULL, + "userAgent" text, + "details" text, + "metadata" jsonb, + "searchText" text NOT NULL, + "createdAt" timestamptz NOT NULL DEFAULT now(), + "archivedAt" timestamptz + ); + `); + + await queryRunner.query( + `CREATE INDEX "idx_audit_user_created" ON "audit_logs" ("userId", "createdAt");`, + ); + await queryRunner.query( + `CREATE INDEX "idx_audit_action_created" ON "audit_logs" ("action", "createdAt");`, + ); + await queryRunner.query( + `CREATE INDEX "idx_audit_ip_created" ON "audit_logs" ("ipAddress", "createdAt");`, + ); + await queryRunner.query( + `CREATE INDEX "idx_audit_created" ON "audit_logs" ("createdAt");`, + ); + + // GIN index over a generated tsvector for sub-2s full-text search at 1M+ rows. + await queryRunner.query(` + CREATE INDEX "idx_audit_search_fts" ON "audit_logs" + USING GIN (to_tsvector('english', "searchText")); + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "audit_logs";`); + await queryRunner.query(`DROP TYPE "audit_logs_action_enum";`); + } +} \ No newline at end of file From e3c239bb92924dc9b07fda48d427f2b745bcebb0 Mon Sep 17 00:00:00 2001 From: blessme247 Date: Tue, 23 Jun 2026 05:00:49 +0100 Subject: [PATCH 2/2] chore: merge new audit log service with old --- src/infrastructure/audit/audit-log.service.ts | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/infrastructure/audit/audit-log.service.ts b/src/infrastructure/audit/audit-log.service.ts index 1e7d16e..b8d55ab 100644 --- a/src/infrastructure/audit/audit-log.service.ts +++ b/src/infrastructure/audit/audit-log.service.ts @@ -15,7 +15,27 @@ const ARCHIVE_AFTER_YEARS = 1; @Injectable() export class AuditLogService { - constructor( + private logs: any[] = []; + + async recordVerification(result: any) { + const entry = { + type: "VERIFICATION", + ...result, + }; + + this.logs.push(entry); + + // ❗ Immutable simulation (append-only) + Object.freeze(entry); + + return entry; + } + + getLogs(limit = 50) { + return this.logs.slice(-limit); + } + + constructor( @InjectRepository(AuditLog) private readonly repo: Repository, private readonly signingService: ExportSigningService, @@ -171,3 +191,4 @@ export class AuditLogService { } +