strapi-cms-audit-log 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +212 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1026 -0
  4. package/dist/admin/App-BLt4lqEM.js +1284 -0
  5. package/dist/admin/App-ou5hy99f.mjs +1266 -0
  6. package/dist/admin/en-B0rPiE2W.mjs +87 -0
  7. package/dist/admin/en-BMlJxq3g.js +87 -0
  8. package/dist/admin/index-BYi8OPTw.js +80 -0
  9. package/dist/admin/index-DdmY-p3Q.mjs +81 -0
  10. package/dist/admin/index.js +4 -0
  11. package/dist/admin/index.mjs +4 -0
  12. package/dist/admin/src/components/AuditLogFilters.d.ts +18 -0
  13. package/dist/admin/src/components/AuditLogTable.d.ts +19 -0
  14. package/dist/admin/src/components/ChangeViewer.d.ts +14 -0
  15. package/dist/admin/src/components/JsonViewer.d.ts +20 -0
  16. package/dist/admin/src/components/PluginIcon.d.ts +3 -0
  17. package/dist/admin/src/components/WidgetDiff.d.ts +19 -0
  18. package/dist/admin/src/hooks/useAuditLogs.d.ts +36 -0
  19. package/dist/admin/src/index.d.ts +3 -0
  20. package/dist/admin/src/pages/App.d.ts +11 -0
  21. package/dist/admin/src/pages/AuditLogDetails.d.ts +10 -0
  22. package/dist/admin/src/pages/AuditLogs.d.ts +3 -0
  23. package/dist/admin/src/permissions.d.ts +26 -0
  24. package/dist/admin/src/pluginId.d.ts +2 -0
  25. package/dist/admin/src/types.d.ts +91 -0
  26. package/dist/admin/src/utils/format.d.ts +41 -0
  27. package/dist/admin/src/utils/getTranslation.d.ts +2 -0
  28. package/dist/admin/src/utils/widgets.d.ts +85 -0
  29. package/dist/server/index.js +1856 -0
  30. package/dist/server/index.mjs +1856 -0
  31. package/dist/server/src/bootstrap.d.ts +14 -0
  32. package/dist/server/src/config/index.d.ts +52 -0
  33. package/dist/server/src/constants.d.ts +140 -0
  34. package/dist/server/src/content-types/audit-log/index.d.ts +86 -0
  35. package/dist/server/src/content-types/audit-log/schema.d.ts +141 -0
  36. package/dist/server/src/content-types/index.d.ts +88 -0
  37. package/dist/server/src/controllers/audit-log.d.ts +31 -0
  38. package/dist/server/src/controllers/index.d.ts +42 -0
  39. package/dist/server/src/destroy.d.ts +19 -0
  40. package/dist/server/src/index.d.ts +328 -0
  41. package/dist/server/src/register.d.ts +31 -0
  42. package/dist/server/src/routes/admin.d.ts +26 -0
  43. package/dist/server/src/routes/index.d.ts +19 -0
  44. package/dist/server/src/services/access.d.ts +51 -0
  45. package/dist/server/src/services/audit.d.ts +25 -0
  46. package/dist/server/src/services/config.d.ts +30 -0
  47. package/dist/server/src/services/context.d.ts +33 -0
  48. package/dist/server/src/services/diff.d.ts +48 -0
  49. package/dist/server/src/services/immutability.d.ts +30 -0
  50. package/dist/server/src/services/index.d.ts +137 -0
  51. package/dist/server/src/services/retention.d.ts +22 -0
  52. package/dist/server/src/services/security.d.ts +54 -0
  53. package/dist/server/src/services/snapshot.d.ts +46 -0
  54. package/dist/server/src/services/tracker.d.ts +39 -0
  55. package/dist/server/src/types/index.d.ts +214 -0
  56. package/dist/server/src/utils/json.d.ts +17 -0
  57. package/dist/server/src/utils/paths.d.ts +34 -0
  58. package/dist/server/src/utils/sanitize.d.ts +16 -0
  59. package/package.json +112 -0
@@ -0,0 +1,328 @@
1
+ /**
2
+ * Server half of the plugin, loaded by Strapi through the `./strapi-server`
3
+ * export in package.json.
4
+ */
5
+ declare const _default: {
6
+ register: ({ strapi }: {
7
+ strapi: import('@strapi/types/dist/core').Strapi;
8
+ }) => Promise<void>;
9
+ bootstrap: ({ strapi }: {
10
+ strapi: import('@strapi/types/dist/core').Strapi;
11
+ }) => void;
12
+ destroy: ({ strapi }: {
13
+ strapi: import('@strapi/types/dist/core').Strapi;
14
+ }) => Promise<void>;
15
+ config: {
16
+ default: {
17
+ enabled: boolean;
18
+ actions: null;
19
+ contentTypes: null;
20
+ ignoredContentTypes: null;
21
+ ignoredFields: null;
22
+ additionalIgnoredFields: null;
23
+ ignoredChangeFields: null;
24
+ storeBefore: boolean;
25
+ storeAfter: boolean;
26
+ storeChanges: boolean;
27
+ retentionDays: number;
28
+ retentionCron: string;
29
+ failOnAuditError: boolean;
30
+ writeMode: string;
31
+ maxPopulateDepth: number;
32
+ maxSnapshotBytes: number;
33
+ auditSystemOperations: boolean;
34
+ securityEvents: null;
35
+ forwardToLogger: boolean;
36
+ forwardLogLevel: string;
37
+ };
38
+ validator(config: import('./types').AuditUserConfig & {
39
+ enabled?: boolean;
40
+ }): void;
41
+ };
42
+ contentTypes: {
43
+ 'audit-log': {
44
+ schema: {
45
+ kind: string;
46
+ collectionName: string;
47
+ info: {
48
+ singularName: string;
49
+ pluralName: string;
50
+ displayName: string;
51
+ description: string;
52
+ };
53
+ options: {
54
+ draftAndPublish: boolean;
55
+ };
56
+ pluginOptions: {
57
+ 'content-manager': {
58
+ visible: boolean;
59
+ };
60
+ 'content-type-builder': {
61
+ visible: boolean;
62
+ };
63
+ };
64
+ attributes: {
65
+ action: {
66
+ type: string;
67
+ required: boolean;
68
+ };
69
+ contentType: {
70
+ type: string;
71
+ required: boolean;
72
+ };
73
+ contentTypeDisplayName: {
74
+ type: string;
75
+ };
76
+ contentDocumentId: {
77
+ type: string;
78
+ };
79
+ contentId: {
80
+ type: string;
81
+ };
82
+ locale: {
83
+ type: string;
84
+ };
85
+ userId: {
86
+ type: string;
87
+ };
88
+ userEmail: {
89
+ type: string;
90
+ };
91
+ userName: {
92
+ type: string;
93
+ };
94
+ changes: {
95
+ type: string;
96
+ };
97
+ before: {
98
+ type: string;
99
+ };
100
+ after: {
101
+ type: string;
102
+ };
103
+ outcome: {
104
+ type: string;
105
+ };
106
+ metadata: {
107
+ type: string;
108
+ };
109
+ ipAddress: {
110
+ type: string;
111
+ };
112
+ userAgent: {
113
+ type: string;
114
+ };
115
+ source: {
116
+ type: string;
117
+ };
118
+ requestId: {
119
+ type: string;
120
+ };
121
+ };
122
+ indexes: {
123
+ name: string;
124
+ columns: string[];
125
+ }[];
126
+ };
127
+ };
128
+ };
129
+ controllers: {
130
+ 'audit-log': ({ strapi }: {
131
+ strapi: import('@strapi/types/dist/core').Strapi;
132
+ }) => {
133
+ find(ctx: {
134
+ query: Record<string, unknown>;
135
+ params: Record<string, string>;
136
+ body: unknown;
137
+ badRequest: (message: string) => unknown;
138
+ notFound: (message?: string) => unknown;
139
+ }): Promise<void>;
140
+ findOne(ctx: {
141
+ query: Record<string, unknown>;
142
+ params: Record<string, string>;
143
+ body: unknown;
144
+ badRequest: (message: string) => unknown;
145
+ notFound: (message?: string) => unknown;
146
+ }): Promise<unknown>;
147
+ delete(ctx: {
148
+ query: Record<string, unknown>;
149
+ params: Record<string, string>;
150
+ body: unknown;
151
+ badRequest: (message: string) => unknown;
152
+ notFound: (message?: string) => unknown;
153
+ }): Promise<unknown>;
154
+ filters(ctx: {
155
+ query: Record<string, unknown>;
156
+ params: Record<string, string>;
157
+ body: unknown;
158
+ badRequest: (message: string) => unknown;
159
+ notFound: (message?: string) => unknown;
160
+ }): Promise<void>;
161
+ config(ctx: {
162
+ query: Record<string, unknown>;
163
+ params: Record<string, string>;
164
+ body: unknown;
165
+ badRequest: (message: string) => unknown;
166
+ notFound: (message?: string) => unknown;
167
+ }): Promise<void>;
168
+ };
169
+ };
170
+ routes: {
171
+ admin: {
172
+ type: string;
173
+ routes: {
174
+ method: string;
175
+ path: string;
176
+ handler: string;
177
+ config: {
178
+ policies: (string | {
179
+ name: string;
180
+ config: {
181
+ actions: string[];
182
+ };
183
+ })[];
184
+ };
185
+ }[];
186
+ };
187
+ };
188
+ services: {
189
+ access: ({ strapi }: {
190
+ strapi: import('@strapi/types/dist/core').Strapi;
191
+ }) => {
192
+ createMiddleware: () => (ctx: {
193
+ [x: string]: any;
194
+ }, next: () => Promise<unknown>) => Promise<void>;
195
+ register: () => void;
196
+ recordDenial: (ctx: {
197
+ [x: string]: any;
198
+ }) => Promise<void>;
199
+ sourceOf: (ctx: {
200
+ [x: string]: any;
201
+ }) => "admin" | "api" | "unknown";
202
+ isIgnoredPath: (path: string) => boolean;
203
+ };
204
+ audit: ({ strapi }: {
205
+ strapi: import('@strapi/types/dist/core').Strapi;
206
+ }) => {
207
+ record: (entry: import('./types').AuditEntryInput) => Promise<void>;
208
+ write: (entry: import('./types').AuditEntryInput) => Promise<void>;
209
+ flush: () => Promise<void>;
210
+ find: (query?: import('./types').AuditLogQuery) => Promise<import('./types').AuditLogListResult>;
211
+ findOne: (id: number) => Promise<import('./types').AuditLog | null>;
212
+ deleteOne: (id: number) => Promise<import('./types').AuditLog | null>;
213
+ deleteOlderThan: (date: Date) => Promise<number>;
214
+ getFilterOptions: () => Promise<import('./types').AuditFilterOptions>;
215
+ buildWhere: (query: import('./types').AuditLogQuery) => Record<string, unknown>;
216
+ parseSort: (sort: string | undefined) => Record<string, "asc" | "desc">;
217
+ };
218
+ config: ({ strapi }: {
219
+ strapi: import('@strapi/types/dist/core').Strapi;
220
+ }) => {
221
+ resolve: () => import('./services/config').ResolvedConfig;
222
+ isAuditedContentType: (uid: string) => boolean;
223
+ isAuditedAction: (action: import('./types').AuditAction) => boolean;
224
+ isAuditedSecurityAction: (action: import('./types').AuditSecurityAction) => boolean;
225
+ hasSecurityEvents: () => boolean;
226
+ enabledSecurityActions: () => import('./types').AuditSecurityAction[];
227
+ getPublicConfig: () => import('./types').AuditConfig;
228
+ clearCache: () => void;
229
+ };
230
+ context: ({ strapi }: {
231
+ strapi: import('@strapi/types/dist/core').Strapi;
232
+ }) => {
233
+ resolve: () => import('./types').AuditContext;
234
+ runAs: <T>(override: import('./services/context').SourceOverride, callback: () => T) => T;
235
+ };
236
+ diff: ({ strapi }: {
237
+ strapi: import('@strapi/types/dist/core').Strapi;
238
+ }) => {
239
+ buildDiff: (before: unknown, after: unknown, options?: import('./services/diff').DiffOptions) => import('./types').AuditChangeSet;
240
+ };
241
+ immutability: ({ strapi }: {
242
+ strapi: import('@strapi/types/dist/core').Strapi;
243
+ }) => {
244
+ createMiddleware: () => (ctx: {
245
+ uid: string;
246
+ action: string;
247
+ }, next: () => Promise<unknown>) => Promise<unknown>;
248
+ register: () => void;
249
+ };
250
+ retention: ({ strapi }: {
251
+ strapi: import('@strapi/types/dist/core').Strapi;
252
+ }) => {
253
+ cleanup: () => Promise<number>;
254
+ cutoffDate: (retentionDays: number, now?: Date) => Date;
255
+ register: () => void;
256
+ unregister: () => void;
257
+ };
258
+ security: ({ strapi }: {
259
+ strapi: import('@strapi/types/dist/core').Strapi;
260
+ }) => {
261
+ register: () => void;
262
+ unregister: () => void;
263
+ record: (action: import('./types').AuditSecurityAction, subject: string, fields?: {
264
+ contentDocumentId?: string | null;
265
+ contentId?: string | null;
266
+ user?: {
267
+ [x: string]: any;
268
+ } | null;
269
+ before?: Record<string, unknown> | null;
270
+ after?: Record<string, unknown> | null;
271
+ metadata?: import('./types').AuditMetadata | null;
272
+ outcome?: import('./types').AuditOutcome;
273
+ }) => Promise<void>;
274
+ identify: (entity: {
275
+ [x: string]: any;
276
+ } | null | undefined) => Record<string, unknown> | null;
277
+ subjectOf: (payload: {
278
+ [x: string]: any;
279
+ }) => {
280
+ [x: string]: any;
281
+ } | null;
282
+ sourceOf: () => "admin" | "system";
283
+ entityHandler: (action: import('./types').AuditSecurityAction, subject: string) => (payload: {
284
+ [x: string]: any;
285
+ }) => Promise<void>;
286
+ handlers: Record<string, (payload: {
287
+ [x: string]: any;
288
+ }) => Promise<void>>;
289
+ };
290
+ snapshot: ({ strapi }: {
291
+ strapi: import('@strapi/types/dist/core').Strapi;
292
+ }) => {
293
+ buildSnapshotQuery: (uid: string, keys: string[] | null, options: {
294
+ depth: number;
295
+ isIgnored?: (path: string) => boolean;
296
+ }) => import('./services/snapshot').SnapshotQuery;
297
+ buildComponentPopulate: (componentUid: string, depth: number) => Record<string, unknown> | true;
298
+ buildWhere: (uid: string, lookup: import('./services/snapshot').SnapshotLookup) => Record<string, unknown>;
299
+ fetchRows: (uid: string, lookup: import('./services/snapshot').SnapshotLookup, query: import('./services/snapshot').SnapshotQuery, depth: number) => Promise<{
300
+ [x: string]: any;
301
+ }[]>;
302
+ refineDynamicZones: (uid: string, rows: {
303
+ [x: string]: any;
304
+ }[], query: import('./services/snapshot').SnapshotQuery, depth: number) => Promise<{
305
+ [x: string]: any;
306
+ }[]>;
307
+ toSnapshot: (row: {
308
+ [x: string]: any;
309
+ } | null | undefined) => Record<string, unknown> | null;
310
+ hasDraftAndPublish: (uid: string) => boolean;
311
+ isLocalized: (uid: string) => boolean;
312
+ };
313
+ tracker: ({ strapi }: {
314
+ strapi: import('@strapi/types/dist/core').Strapi;
315
+ }) => {
316
+ createMiddleware: () => (ctx: import('./services/tracker').DocumentMiddlewareContext, next: () => Promise<unknown>) => Promise<unknown>;
317
+ register: () => void;
318
+ changedKeys: (uid: string, params: Record<string, any>) => string[] | null;
319
+ resultRows: (result: unknown) => {
320
+ [x: string]: any;
321
+ }[];
322
+ };
323
+ };
324
+ };
325
+ export default _default;
326
+ /** Public type surface — see `server/src/types`. */
327
+ export type { AuditAction, AuditActor, AuditAnyAction, AuditChange, AuditChangeSet, AuditConfig, AuditContext, AuditEntryInput, AuditFilterOptions, AuditLog, AuditLogListResult, AuditLogQuery, AuditLogLevel, AuditMetadata, AuditOutcome, AuditRequestContext, AuditSecurityAction, AuditSource, AuditUserConfig, AuditWriteMode, ContentTypeSelector, SecurityEventSelector, } from './types';
328
+ export { PLUGIN_ID, AUDIT_LOG_UID, PERMISSIONS, DEFAULT_IGNORED_FIELDS, CONTENT_ACTIONS, ALL_SECURITY_ACTIONS, SECURITY_EVENT_MAP, SUBJECTS, } from './constants';
@@ -0,0 +1,31 @@
1
+ import { Core } from '@strapi/strapi';
2
+ /**
3
+ * RBAC actions this plugin contributes.
4
+ *
5
+ * `section: 'plugins'` puts them in the Plugins tab of the role editor, next to
6
+ * the other plugins, and the provider namespaces each `uid` with the plugin name
7
+ * — `read` becomes `plugin::audit-log.read`.
8
+ *
9
+ * Read and delete are separate on purpose: an auditor needs to see the trail, an
10
+ * auditor must not be able to erase it, and collapsing the two into one
11
+ * permission would make that distinction unexpressible.
12
+ */
13
+ declare const ACTIONS: {
14
+ section: string;
15
+ displayName: string;
16
+ uid: string;
17
+ pluginName: string;
18
+ }[];
19
+ /**
20
+ * Runs before content types are loaded and before `bootstrap`.
21
+ *
22
+ * Permissions are registered here rather than in `bootstrap` so the actions
23
+ * exist by the time the admin builds its permission sections — registering them
24
+ * later leaves the role editor with nothing to tick on the first boot after
25
+ * install.
26
+ */
27
+ declare const register: ({ strapi }: {
28
+ strapi: Core.Strapi;
29
+ }) => Promise<void>;
30
+ export default register;
31
+ export { ACTIONS };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Admin API for the plugin.
3
+ *
4
+ * `type: 'admin'` mounts these under the admin server and prefixes them with the
5
+ * plugin id, so the paths below become `/audit-log/logs`, `/audit-log/logs/:id`
6
+ * and so on. There is no `type: 'content-api'` route file, and that is the
7
+ * point: audit records are never reachable from the public content API, with or
8
+ * without an API token, because no public route exists to reach them.
9
+ */
10
+ declare const _default: {
11
+ type: string;
12
+ routes: {
13
+ method: string;
14
+ path: string;
15
+ handler: string;
16
+ config: {
17
+ policies: (string | {
18
+ name: string;
19
+ config: {
20
+ actions: string[];
21
+ };
22
+ })[];
23
+ };
24
+ }[];
25
+ };
26
+ export default _default;
@@ -0,0 +1,19 @@
1
+ declare const _default: {
2
+ admin: {
3
+ type: string;
4
+ routes: {
5
+ method: string;
6
+ path: string;
7
+ handler: string;
8
+ config: {
9
+ policies: (string | {
10
+ name: string;
11
+ config: {
12
+ actions: string[];
13
+ };
14
+ })[];
15
+ };
16
+ }[];
17
+ };
18
+ };
19
+ export default _default;
@@ -0,0 +1,51 @@
1
+ import { Core } from '@strapi/strapi';
2
+ type KoaContext = Record<string, any>;
3
+ /**
4
+ * Records requests Strapi refused: `401 Unauthorized` and `403 Forbidden`.
5
+ *
6
+ * This is the "unauthorized access / permission failures" line of a security
7
+ * review, and it is the one part of the audit trail that cannot come from an
8
+ * event, because Strapi emits none for a refusal. A denial is not a write, has
9
+ * no entity and touches no content type — it exists only as an HTTP status.
10
+ *
11
+ * ## Why this middleware sits outermost, and how it gets there
12
+ *
13
+ * `strapi.server.use()` appends to the Koa stack, and Strapi's boot order is:
14
+ *
15
+ * ```
16
+ * register() <- plugin register lifecycles run here
17
+ * bootstrap()
18
+ * server.initMiddlewares() <- config/middlewares.ts is applied
19
+ * server.initRouting() <- routes and their policies are mounted
20
+ * bootstrap lifecycles <- plugin bootstrap runs here
21
+ * ```
22
+ *
23
+ * Registered from the plugin's `register`, this lands ahead of every configured
24
+ * middleware and ahead of the router — so by the time `await next()` returns,
25
+ * `strapi::errors` has already turned any thrown `ForbiddenError` into a status
26
+ * and `ctx.status` is final. Registered from `bootstrap` it would land *after*
27
+ * the router, see nothing, and fail silently. That ordering is the entire reason
28
+ * this service is wired up in `register.ts` rather than alongside the others in
29
+ * `bootstrap.ts`.
30
+ *
31
+ * ## What is deliberately not recorded
32
+ *
33
+ * A failed login is a 401, and it already produces a far better record from
34
+ * `admin.auth.error` — one that names the account that was tried. Recording the
35
+ * status too would double every wrong-password attempt, so the login and
36
+ * token-refresh paths are excluded by {@link ACCESS_IGNORED_PATHS}. 404s are
37
+ * excluded as well: Strapi answers an unauthorised *content API* read with a
38
+ * 404 rather than a 403 to avoid confirming that a document exists, and
39
+ * recording every 404 to catch those would bury the log in typos and favicon
40
+ * requests.
41
+ */
42
+ declare const accessService: ({ strapi }: {
43
+ strapi: Core.Strapi;
44
+ }) => {
45
+ createMiddleware: () => (ctx: KoaContext, next: () => Promise<unknown>) => Promise<void>;
46
+ register: () => void;
47
+ recordDenial: (ctx: KoaContext) => Promise<void>;
48
+ sourceOf: (ctx: KoaContext) => "admin" | "api" | "unknown";
49
+ isIgnoredPath: (path: string) => boolean;
50
+ };
51
+ export default accessService;
@@ -0,0 +1,25 @@
1
+ import { Core } from '@strapi/strapi';
2
+ import { AuditEntryInput, AuditFilterOptions, AuditLog, AuditLogListResult, AuditLogQuery } from '../types';
3
+ /**
4
+ * Persistence and retrieval of audit records.
5
+ *
6
+ * Every database call goes through `strapi.db.query`, never the Document
7
+ * Service. That is deliberate on both ends: writes must not re-enter the
8
+ * middleware that produced them, and reads must not be reshaped by document
9
+ * middlewares a consuming application has registered for its own content.
10
+ */
11
+ declare const auditService: ({ strapi }: {
12
+ strapi: Core.Strapi;
13
+ }) => {
14
+ record: (entry: AuditEntryInput) => Promise<void>;
15
+ write: (entry: AuditEntryInput) => Promise<void>;
16
+ flush: () => Promise<void>;
17
+ find: (query?: AuditLogQuery) => Promise<AuditLogListResult>;
18
+ findOne: (id: number) => Promise<AuditLog | null>;
19
+ deleteOne: (id: number) => Promise<AuditLog | null>;
20
+ deleteOlderThan: (date: Date) => Promise<number>;
21
+ getFilterOptions: () => Promise<AuditFilterOptions>;
22
+ buildWhere: (query: AuditLogQuery) => Record<string, unknown>;
23
+ parseSort: (sort: string | undefined) => Record<string, "asc" | "desc">;
24
+ };
25
+ export default auditService;
@@ -0,0 +1,30 @@
1
+ import { Core } from '@strapi/strapi';
2
+ import { PathMatcher } from '../utils/sanitize';
3
+ import { AuditAction, AuditConfig, AuditSecurityAction } from '../types';
4
+ export interface ResolvedConfig extends AuditConfig {
5
+ /** True when `changes` is excluded from a path as well as `before`/`after`. */
6
+ isIgnoredForChanges: PathMatcher;
7
+ /** True when a path must never be persisted anywhere. */
8
+ isIgnoredForSnapshot: PathMatcher;
9
+ }
10
+ /**
11
+ * Resolves and caches the effective configuration.
12
+ *
13
+ * Cached because it is read on the hot path — once per audited write — and
14
+ * because `createPathMatcher` compiles the ignore patterns. Nothing invalidates
15
+ * it: Strapi's plugin config is fixed at boot, so a cache miss can only happen
16
+ * once per process.
17
+ */
18
+ declare const configService: ({ strapi }: {
19
+ strapi: Core.Strapi;
20
+ }) => {
21
+ resolve: () => ResolvedConfig;
22
+ isAuditedContentType: (uid: string) => boolean;
23
+ isAuditedAction: (action: AuditAction) => boolean;
24
+ isAuditedSecurityAction: (action: AuditSecurityAction) => boolean;
25
+ hasSecurityEvents: () => boolean;
26
+ enabledSecurityActions: () => AuditSecurityAction[];
27
+ getPublicConfig: () => AuditConfig;
28
+ clearCache: () => void;
29
+ };
30
+ export default configService;
@@ -0,0 +1,33 @@
1
+ import { Core } from '@strapi/strapi';
2
+ import { AuditContext, AuditSource } from '../types';
3
+ /**
4
+ * Overrides for code that runs outside a request.
5
+ *
6
+ * A migration script or a cron task has no Koa context, so without this every
7
+ * such write would land as `source: "system"` with no actor. `runAs` lets the
8
+ * consuming application label its own background work without the plugin having
9
+ * to guess, and without any caller-supplied value ever reaching an HTTP request
10
+ * — this store is process-local and can only be written from server code.
11
+ */
12
+ export interface SourceOverride {
13
+ source: AuditSource;
14
+ userId?: string | null;
15
+ userEmail?: string | null;
16
+ userName?: string | null;
17
+ }
18
+ /**
19
+ * Derives who did what, from where.
20
+ *
21
+ * Everything comes from the server-side Koa context that Strapi keeps in
22
+ * AsyncLocalStorage (`strapi.requestContext`). Nothing is read from the request
23
+ * body or from a client-settable header other than the correlation id, which is
24
+ * treated as an opaque label and never used for authorisation — a client that
25
+ * lies about it can only confuse its own trace.
26
+ */
27
+ declare const contextService: ({ strapi }: {
28
+ strapi: Core.Strapi;
29
+ }) => {
30
+ resolve: () => AuditContext;
31
+ runAs: <T>(override: SourceOverride, callback: () => T) => T;
32
+ };
33
+ export default contextService;
@@ -0,0 +1,48 @@
1
+ import { Core } from '@strapi/strapi';
2
+ import { PathMatcher } from '../utils/sanitize';
3
+ import { AuditChangeSet } from '../types';
4
+ export interface DiffOptions {
5
+ /** Paths excluded from the result. Already applied to `before`/`after` for the sensitive list; this one also carries `ignoredChangeFields`. */
6
+ isIgnored?: PathMatcher;
7
+ /**
8
+ * Restricts the diff to these top-level keys.
9
+ *
10
+ * For an update this is the set of attributes the caller actually sent, which
11
+ * is what makes the diff both meaningful and cheap: comparing keys the write
12
+ * never touched can only produce noise, and `before`/`after` were only
13
+ * populated for these keys in the first place.
14
+ */
15
+ keys?: string[] | null;
16
+ /**
17
+ * Longest path, in segments, before a whole subtree is recorded as one change.
18
+ *
19
+ * `maxDepth: 2` produces `seo.metaTitle` but collapses `a.b.c.d` into a single
20
+ * `a.b` entry holding both subtrees. A dynamic zone nested three components
21
+ * deep is still legible as `blocks[2].rows[0].label`; past that, a reader is
22
+ * better served by the raw JSON of the subtree than by a hundred leaf paths.
23
+ */
24
+ maxDepth?: number;
25
+ /** Ceiling on entries, so one pathological write cannot produce a megabyte of diff. */
26
+ maxChanges?: number;
27
+ }
28
+ /**
29
+ * Field-level diff between two snapshots.
30
+ *
31
+ * Entirely generic: it knows nothing about content types, components or dynamic
32
+ * zones, and works purely off the shape of the two values. That is deliberate —
33
+ * anything schema-aware would have to walk every component definition a type
34
+ * *could* hold, which for a page with 173 registered widgets is exactly the
35
+ * work this plugin exists to avoid.
36
+ *
37
+ * Arrays — repeatable components, dynamic zones, multi-relations, media lists —
38
+ * are compared by index and addressed as `blocks[2].heading`. Index comparison
39
+ * reports a reorder as a change to every moved element, which is honest: the
40
+ * stored order *is* the content, and a reader looking at "why did this page
41
+ * change" wants to see that the blocks moved.
42
+ */
43
+ declare const diffService: ({ strapi }: {
44
+ strapi: Core.Strapi;
45
+ }) => {
46
+ buildDiff: (before: unknown, after: unknown, options?: DiffOptions) => AuditChangeSet;
47
+ };
48
+ export default diffService;
@@ -0,0 +1,30 @@
1
+ import { Core } from '@strapi/strapi';
2
+ /**
3
+ * Second line of defence for audit-record immutability.
4
+ *
5
+ * The first line is structural: `content-manager: { visible: false }` on the
6
+ * schema means the Content Manager never registers create/update/delete RBAC
7
+ * actions for this content type, so there is no permission an administrator
8
+ * could grant that would let a role write these rows through the normal content
9
+ * APIs. That covers the panel and the content API.
10
+ *
11
+ * It does not cover *server* code. A plugin, a migration or a stray
12
+ * `strapi.documents('plugin::audit-log.audit-log').update(...)` in a bootstrap
13
+ * would otherwise succeed silently. This middleware makes that a loud failure.
14
+ *
15
+ * Deletion is not blocked here. It is a legitimate operation — retention runs on
16
+ * it — and it is gated where it belongs: the only HTTP route that deletes
17
+ * requires `plugin::audit-log.delete`. The plugin's own writes and deletes go
18
+ * through `strapi.db.query`, below the Document Service, so this guard never
19
+ * needs an escape hatch that an attacker could reach for.
20
+ */
21
+ declare const immutabilityService: ({ strapi }: {
22
+ strapi: Core.Strapi;
23
+ }) => {
24
+ createMiddleware: () => (ctx: {
25
+ uid: string;
26
+ action: string;
27
+ }, next: () => Promise<unknown>) => Promise<unknown>;
28
+ register: () => void;
29
+ };
30
+ export default immutabilityService;