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.
- package/CHANGELOG.md +212 -0
- package/LICENSE +21 -0
- package/README.md +1026 -0
- package/dist/admin/App-BLt4lqEM.js +1284 -0
- package/dist/admin/App-ou5hy99f.mjs +1266 -0
- package/dist/admin/en-B0rPiE2W.mjs +87 -0
- package/dist/admin/en-BMlJxq3g.js +87 -0
- package/dist/admin/index-BYi8OPTw.js +80 -0
- package/dist/admin/index-DdmY-p3Q.mjs +81 -0
- package/dist/admin/index.js +4 -0
- package/dist/admin/index.mjs +4 -0
- package/dist/admin/src/components/AuditLogFilters.d.ts +18 -0
- package/dist/admin/src/components/AuditLogTable.d.ts +19 -0
- package/dist/admin/src/components/ChangeViewer.d.ts +14 -0
- package/dist/admin/src/components/JsonViewer.d.ts +20 -0
- package/dist/admin/src/components/PluginIcon.d.ts +3 -0
- package/dist/admin/src/components/WidgetDiff.d.ts +19 -0
- package/dist/admin/src/hooks/useAuditLogs.d.ts +36 -0
- package/dist/admin/src/index.d.ts +3 -0
- package/dist/admin/src/pages/App.d.ts +11 -0
- package/dist/admin/src/pages/AuditLogDetails.d.ts +10 -0
- package/dist/admin/src/pages/AuditLogs.d.ts +3 -0
- package/dist/admin/src/permissions.d.ts +26 -0
- package/dist/admin/src/pluginId.d.ts +2 -0
- package/dist/admin/src/types.d.ts +91 -0
- package/dist/admin/src/utils/format.d.ts +41 -0
- package/dist/admin/src/utils/getTranslation.d.ts +2 -0
- package/dist/admin/src/utils/widgets.d.ts +85 -0
- package/dist/server/index.js +1856 -0
- package/dist/server/index.mjs +1856 -0
- package/dist/server/src/bootstrap.d.ts +14 -0
- package/dist/server/src/config/index.d.ts +52 -0
- package/dist/server/src/constants.d.ts +140 -0
- package/dist/server/src/content-types/audit-log/index.d.ts +86 -0
- package/dist/server/src/content-types/audit-log/schema.d.ts +141 -0
- package/dist/server/src/content-types/index.d.ts +88 -0
- package/dist/server/src/controllers/audit-log.d.ts +31 -0
- package/dist/server/src/controllers/index.d.ts +42 -0
- package/dist/server/src/destroy.d.ts +19 -0
- package/dist/server/src/index.d.ts +328 -0
- package/dist/server/src/register.d.ts +31 -0
- package/dist/server/src/routes/admin.d.ts +26 -0
- package/dist/server/src/routes/index.d.ts +19 -0
- package/dist/server/src/services/access.d.ts +51 -0
- package/dist/server/src/services/audit.d.ts +25 -0
- package/dist/server/src/services/config.d.ts +30 -0
- package/dist/server/src/services/context.d.ts +33 -0
- package/dist/server/src/services/diff.d.ts +48 -0
- package/dist/server/src/services/immutability.d.ts +30 -0
- package/dist/server/src/services/index.d.ts +137 -0
- package/dist/server/src/services/retention.d.ts +22 -0
- package/dist/server/src/services/security.d.ts +54 -0
- package/dist/server/src/services/snapshot.d.ts +46 -0
- package/dist/server/src/services/tracker.d.ts +39 -0
- package/dist/server/src/types/index.d.ts +214 -0
- package/dist/server/src/utils/json.d.ts +17 -0
- package/dist/server/src/utils/paths.d.ts +34 -0
- package/dist/server/src/utils/sanitize.d.ts +16 -0
- 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;
|