strapi-cms-audit-log 1.1.0 → 1.2.1
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 +78 -0
- package/README.md +12 -12
- package/dist/admin/{App-BLt4lqEM.js → App-CmbG2k9E.js} +110 -100
- package/dist/admin/{App-ou5hy99f.mjs → App-jWvaMXIw.mjs} +113 -103
- package/dist/admin/{en-BMlJxq3g.js → en-Bs4tlklI.js} +11 -9
- package/dist/admin/{en-B0rPiE2W.mjs → en-C3Og1VyK.mjs} +11 -9
- package/dist/admin/{index-DdmY-p3Q.mjs → index-BIIMJgKU.mjs} +3 -5
- package/dist/admin/{index-BYi8OPTw.js → index-BsVOGSDf.js} +3 -5
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/admin/src/components/AuditLogTable.d.ts +1 -2
- package/dist/admin/src/components/IntegrityStatus.d.ts +13 -0
- package/dist/admin/src/hooks/useAuditLogs.d.ts +14 -3
- package/dist/admin/src/permissions.d.ts +0 -8
- package/dist/admin/src/types.d.ts +26 -2
- package/dist/server/index.js +321 -94
- package/dist/server/index.mjs +321 -94
- package/dist/server/src/config/index.d.ts +1 -0
- package/dist/server/src/constants.d.ts +24 -2
- package/dist/server/src/content-types/audit-log/index.d.ts +8 -0
- package/dist/server/src/content-types/audit-log/schema.d.ts +21 -0
- package/dist/server/src/content-types/index.d.ts +8 -0
- package/dist/server/src/controllers/audit-log.d.ts +5 -3
- package/dist/server/src/controllers/index.d.ts +4 -4
- package/dist/server/src/index.d.ts +36 -7
- package/dist/server/src/register.d.ts +7 -3
- package/dist/server/src/routes/admin.d.ts +8 -0
- package/dist/server/src/services/audit.d.ts +0 -1
- package/dist/server/src/services/correlation.d.ts +35 -0
- package/dist/server/src/services/immutability.d.ts +5 -5
- package/dist/server/src/services/index.d.ts +21 -1
- package/dist/server/src/services/integrity.d.ts +69 -0
- package/dist/server/src/types/index.d.ts +54 -4
- package/package.json +10 -6
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useFetchClient, Page, Layouts, BackButton,
|
|
2
|
+
import { useFetchClient, Page, Layouts, BackButton, useQueryParams, SearchInput, Pagination } from "@strapi/strapi/admin";
|
|
3
3
|
import { useParams, useNavigate, Routes, Route } from "react-router-dom";
|
|
4
|
-
import { g as getTranslation, P as PLUGIN_ID, a as PERMISSIONS } from "./index-
|
|
5
|
-
import { Box, Flex, Typography, Button, Badge, Divider, Grid, Field, SingleSelect, SingleSelectOption, Combobox, ComboboxOption, TextInput, DatePicker, EmptyStateLayout, Table, Thead, Tr, Th, VisuallyHidden, Tbody, Td, IconButton
|
|
4
|
+
import { g as getTranslation, P as PLUGIN_ID, a as PERMISSIONS } from "./index-BIIMJgKU.mjs";
|
|
5
|
+
import { Box, Flex, Typography, Button, Badge, Divider, Grid, Field, SingleSelect, SingleSelectOption, Combobox, ComboboxOption, TextInput, DatePicker, EmptyStateLayout, Table, Thead, Tr, Th, VisuallyHidden, Tbody, Td, IconButton } from "@strapi/design-system";
|
|
6
6
|
import { useIntl } from "react-intl";
|
|
7
7
|
import * as React from "react";
|
|
8
|
-
import { CaretUp, CaretDown, Cross, Eye
|
|
8
|
+
import { CaretUp, CaretDown, Cross, Eye } from "@strapi/icons";
|
|
9
9
|
const ACTION_COLORS = {
|
|
10
10
|
create: { background: "success100", text: "success600" },
|
|
11
11
|
update: { background: "primary100", text: "primary600" },
|
|
@@ -33,7 +33,9 @@ const ACTION_COLORS = {
|
|
|
33
33
|
"media.delete": { background: "danger100", text: "danger600" },
|
|
34
34
|
"media-folder.create": { background: "success100", text: "success600" },
|
|
35
35
|
"media-folder.update": { background: "primary100", text: "primary600" },
|
|
36
|
-
"media-folder.delete": { background: "danger100", text: "danger600" }
|
|
36
|
+
"media-folder.delete": { background: "danger100", text: "danger600" },
|
|
37
|
+
// The log's own housekeeping. Neutral: expected, scheduled, and self-reported.
|
|
38
|
+
"retention.purge": { background: "neutral150", text: "neutral700" }
|
|
37
39
|
};
|
|
38
40
|
const actionColor = (action) => ACTION_COLORS[action] ?? { background: "neutral150", text: "neutral700" };
|
|
39
41
|
const shortContentTypeName = (uid) => {
|
|
@@ -568,6 +570,30 @@ const useAuditFilterOptions = () => {
|
|
|
568
570
|
}, [get]);
|
|
569
571
|
return { options, isLoading };
|
|
570
572
|
};
|
|
573
|
+
const useAuditIntegrity = () => {
|
|
574
|
+
const { get } = useFetchClient();
|
|
575
|
+
const [data, setData] = React.useState(null);
|
|
576
|
+
const [isLoading, setIsLoading] = React.useState(true);
|
|
577
|
+
const [error, setError] = React.useState(null);
|
|
578
|
+
React.useEffect(() => {
|
|
579
|
+
let cancelled = false;
|
|
580
|
+
const load = async () => {
|
|
581
|
+
try {
|
|
582
|
+
const { data: response } = await get(`/${PLUGIN_ID}/integrity`);
|
|
583
|
+
if (!cancelled) setData(response.data);
|
|
584
|
+
} catch (err) {
|
|
585
|
+
if (!cancelled) setError(message(err));
|
|
586
|
+
} finally {
|
|
587
|
+
if (!cancelled) setIsLoading(false);
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
void load();
|
|
591
|
+
return () => {
|
|
592
|
+
cancelled = true;
|
|
593
|
+
};
|
|
594
|
+
}, [get]);
|
|
595
|
+
return { data, isLoading, error };
|
|
596
|
+
};
|
|
571
597
|
const useAuditLog = (id) => {
|
|
572
598
|
const { get } = useFetchClient();
|
|
573
599
|
const [data, setData] = React.useState(null);
|
|
@@ -602,15 +628,6 @@ const useAuditLog = (id) => {
|
|
|
602
628
|
}, [get, id]);
|
|
603
629
|
return { data, isLoading, error };
|
|
604
630
|
};
|
|
605
|
-
const useDeleteAuditLog = () => {
|
|
606
|
-
const { del } = useFetchClient();
|
|
607
|
-
return React.useCallback(
|
|
608
|
-
async (id) => {
|
|
609
|
-
await del(`/${PLUGIN_ID}/logs/${id}`);
|
|
610
|
-
},
|
|
611
|
-
[del]
|
|
612
|
-
);
|
|
613
|
-
};
|
|
614
631
|
const MetaField = ({ label, value, mono = false }) => /* @__PURE__ */ jsxs(Grid.Item, { col: 4, s: 6, xs: 12, direction: "column", alignItems: "flex-start", gap: 1, children: [
|
|
615
632
|
/* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: label }),
|
|
616
633
|
/* @__PURE__ */ jsx(
|
|
@@ -786,6 +803,25 @@ const AuditLogDetails = () => {
|
|
|
786
803
|
value: log.userAgent,
|
|
787
804
|
mono: true
|
|
788
805
|
}
|
|
806
|
+
),
|
|
807
|
+
/* @__PURE__ */ jsx(
|
|
808
|
+
MetaField,
|
|
809
|
+
{
|
|
810
|
+
label: formatMessage({ id: getTranslation("detail.hash"), defaultMessage: "Hash" }),
|
|
811
|
+
value: log.hash,
|
|
812
|
+
mono: true
|
|
813
|
+
}
|
|
814
|
+
),
|
|
815
|
+
/* @__PURE__ */ jsx(
|
|
816
|
+
MetaField,
|
|
817
|
+
{
|
|
818
|
+
label: formatMessage({
|
|
819
|
+
id: getTranslation("detail.prevHash"),
|
|
820
|
+
defaultMessage: "Previous hash"
|
|
821
|
+
}),
|
|
822
|
+
value: log.prevHash,
|
|
823
|
+
mono: true
|
|
824
|
+
}
|
|
789
825
|
)
|
|
790
826
|
] }) })
|
|
791
827
|
] }),
|
|
@@ -1009,7 +1045,6 @@ const AuditLogTable = ({
|
|
|
1009
1045
|
sort,
|
|
1010
1046
|
onSortChange,
|
|
1011
1047
|
onView,
|
|
1012
|
-
onDelete,
|
|
1013
1048
|
isLoading
|
|
1014
1049
|
}) => {
|
|
1015
1050
|
const { formatMessage, locale } = useIntl();
|
|
@@ -1064,36 +1099,69 @@ const AuditLogTable = ({
|
|
|
1064
1099
|
/* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral800", children: log.locale ?? "-" }) }),
|
|
1065
1100
|
/* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: log.source }) }),
|
|
1066
1101
|
/* @__PURE__ */ jsx(Td, { children: log.outcome === "failure" ? /* @__PURE__ */ jsx(Badge, { backgroundColor: "danger100", textColor: "danger600", children: log.outcome }) : /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: log.outcome ?? "success" }) }),
|
|
1067
|
-
/* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
{
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
),
|
|
1080
|
-
onDelete ? /* @__PURE__ */ jsx(
|
|
1081
|
-
IconButton,
|
|
1082
|
-
{
|
|
1083
|
-
label: formatMessage({
|
|
1084
|
-
id: getTranslation("list.delete"),
|
|
1085
|
-
defaultMessage: "Delete this record"
|
|
1086
|
-
}),
|
|
1087
|
-
variant: "ghost",
|
|
1088
|
-
onClick: () => onDelete(log),
|
|
1089
|
-
children: /* @__PURE__ */ jsx(Trash, {})
|
|
1090
|
-
}
|
|
1091
|
-
) : null
|
|
1092
|
-
] }) })
|
|
1102
|
+
/* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Flex, { gap: 1, justifyContent: "flex-end", onClick: (event) => event.stopPropagation(), children: /* @__PURE__ */ jsx(
|
|
1103
|
+
IconButton,
|
|
1104
|
+
{
|
|
1105
|
+
label: formatMessage({
|
|
1106
|
+
id: getTranslation("list.view"),
|
|
1107
|
+
defaultMessage: "View details"
|
|
1108
|
+
}),
|
|
1109
|
+
variant: "ghost",
|
|
1110
|
+
onClick: () => onView(log),
|
|
1111
|
+
children: /* @__PURE__ */ jsx(Eye, {})
|
|
1112
|
+
}
|
|
1113
|
+
) }) })
|
|
1093
1114
|
] }, log.id);
|
|
1094
1115
|
}) })
|
|
1095
1116
|
] });
|
|
1096
1117
|
};
|
|
1118
|
+
const IntegrityStatus = () => {
|
|
1119
|
+
const { formatMessage, locale } = useIntl();
|
|
1120
|
+
const { data, isLoading, error } = useAuditIntegrity();
|
|
1121
|
+
if (isLoading) {
|
|
1122
|
+
return /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: formatMessage({
|
|
1123
|
+
id: getTranslation("integrity.checking"),
|
|
1124
|
+
defaultMessage: "Verifying the audit chain…"
|
|
1125
|
+
}) });
|
|
1126
|
+
}
|
|
1127
|
+
if (error || !data) {
|
|
1128
|
+
return /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "warning600", children: formatMessage(
|
|
1129
|
+
{
|
|
1130
|
+
id: getTranslation("integrity.unavailable"),
|
|
1131
|
+
defaultMessage: "Integrity check unavailable: {error}"
|
|
1132
|
+
},
|
|
1133
|
+
{ error: error ?? "no report" }
|
|
1134
|
+
) });
|
|
1135
|
+
}
|
|
1136
|
+
if (!data.ok) {
|
|
1137
|
+
return /* @__PURE__ */ jsxs(Box, { background: "danger100", hasRadius: true, padding: 3, borderColor: "danger200", borderWidth: "1px", borderStyle: "solid", children: [
|
|
1138
|
+
/* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, wrap: "wrap", children: [
|
|
1139
|
+
/* @__PURE__ */ jsx(Badge, { backgroundColor: "danger600", textColor: "neutral0", children: formatMessage({ id: getTranslation("integrity.broken"), defaultMessage: "CHAIN BROKEN" }) }),
|
|
1140
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "danger700", fontWeight: "bold", children: formatMessage(
|
|
1141
|
+
{
|
|
1142
|
+
id: getTranslation("integrity.brokenAt"),
|
|
1143
|
+
defaultMessage: "Record #{id}: {reason}"
|
|
1144
|
+
},
|
|
1145
|
+
{ id: data.brokenAt?.id ?? "?", reason: data.brokenAt?.reason ?? "" }
|
|
1146
|
+
) })
|
|
1147
|
+
] }),
|
|
1148
|
+
/* @__PURE__ */ jsx(Box, { paddingTop: 1, children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "danger700", children: formatMessage({
|
|
1149
|
+
id: getTranslation("integrity.brokenHint"),
|
|
1150
|
+
defaultMessage: "A record was modified, removed or inserted after it was written. Treat everything from this point as unverified and compare against the forwarded copy."
|
|
1151
|
+
}) }) })
|
|
1152
|
+
] });
|
|
1153
|
+
}
|
|
1154
|
+
return /* @__PURE__ */ jsxs(Flex, { alignItems: "center", gap: 2, wrap: "wrap", children: [
|
|
1155
|
+
/* @__PURE__ */ jsx(Badge, { backgroundColor: "success100", textColor: "success700", children: formatMessage({ id: getTranslation("integrity.ok"), defaultMessage: "Chain verified" }) }),
|
|
1156
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: formatMessage(
|
|
1157
|
+
{
|
|
1158
|
+
id: getTranslation("integrity.summary"),
|
|
1159
|
+
defaultMessage: "{hashed, plural, one {# linked record} other {# linked records}}{legacy, plural, =0 {} one { · # pre-chain record} other { · # pre-chain records}} · checked {when}"
|
|
1160
|
+
},
|
|
1161
|
+
{ hashed: data.hashed, legacy: data.legacy, when: formatDate(data.verifiedAt, locale) }
|
|
1162
|
+
) })
|
|
1163
|
+
] });
|
|
1164
|
+
};
|
|
1097
1165
|
const toSearchParams = (query) => {
|
|
1098
1166
|
const params = new URLSearchParams();
|
|
1099
1167
|
for (const [key, value] of Object.entries(query)) {
|
|
@@ -1105,17 +1173,10 @@ const toSearchParams = (query) => {
|
|
|
1105
1173
|
const AuditLogs = () => {
|
|
1106
1174
|
const { formatMessage } = useIntl();
|
|
1107
1175
|
const navigate = useNavigate();
|
|
1108
|
-
const { toggleNotification } = useNotification();
|
|
1109
1176
|
const [{ query }, setQuery] = useQueryParams({ page: 1, pageSize: 20 });
|
|
1110
|
-
const {
|
|
1111
|
-
allowedActions: { canDelete },
|
|
1112
|
-
isLoading: isLoadingPermissions
|
|
1113
|
-
} = useRBAC(PERMISSIONS);
|
|
1114
1177
|
const search = React.useMemo(() => toSearchParams(query), [query]);
|
|
1115
|
-
const { data, pagination, isLoading, error
|
|
1178
|
+
const { data, pagination, isLoading, error } = useAuditLogs(search);
|
|
1116
1179
|
const { options } = useAuditFilterOptions();
|
|
1117
|
-
const deleteLog = useDeleteAuditLog();
|
|
1118
|
-
const [pendingDelete, setPendingDelete] = React.useState(null);
|
|
1119
1180
|
const updateQuery = React.useCallback(
|
|
1120
1181
|
(patch) => {
|
|
1121
1182
|
setQuery({ ...patch, page: 1 });
|
|
@@ -1139,33 +1200,6 @@ const AuditLogs = () => {
|
|
|
1139
1200
|
"remove"
|
|
1140
1201
|
);
|
|
1141
1202
|
}, [setQuery]);
|
|
1142
|
-
const confirmDelete = React.useCallback(async () => {
|
|
1143
|
-
if (!pendingDelete) return;
|
|
1144
|
-
try {
|
|
1145
|
-
await deleteLog(pendingDelete.id);
|
|
1146
|
-
toggleNotification({
|
|
1147
|
-
type: "success",
|
|
1148
|
-
message: formatMessage({
|
|
1149
|
-
id: getTranslation("delete.success"),
|
|
1150
|
-
defaultMessage: "Audit record deleted."
|
|
1151
|
-
})
|
|
1152
|
-
});
|
|
1153
|
-
refresh();
|
|
1154
|
-
} catch {
|
|
1155
|
-
toggleNotification({
|
|
1156
|
-
type: "danger",
|
|
1157
|
-
message: formatMessage({
|
|
1158
|
-
id: getTranslation("delete.error"),
|
|
1159
|
-
defaultMessage: "Could not delete this audit record."
|
|
1160
|
-
})
|
|
1161
|
-
});
|
|
1162
|
-
} finally {
|
|
1163
|
-
setPendingDelete(null);
|
|
1164
|
-
}
|
|
1165
|
-
}, [deleteLog, formatMessage, pendingDelete, refresh, toggleNotification]);
|
|
1166
|
-
if (isLoadingPermissions) {
|
|
1167
|
-
return /* @__PURE__ */ jsx(Page.Loading, {});
|
|
1168
|
-
}
|
|
1169
1203
|
return /* @__PURE__ */ jsxs(Page.Main, { children: [
|
|
1170
1204
|
/* @__PURE__ */ jsx(Page.Title, { children: formatMessage({ id: getTranslation("plugin.name"), defaultMessage: "Audit Logs" }) }),
|
|
1171
1205
|
/* @__PURE__ */ jsx(
|
|
@@ -1196,6 +1230,7 @@ const AuditLogs = () => {
|
|
|
1196
1230
|
}
|
|
1197
1231
|
),
|
|
1198
1232
|
/* @__PURE__ */ jsxs(Layouts.Content, { children: [
|
|
1233
|
+
/* @__PURE__ */ jsx(Box, { paddingBottom: 4, children: /* @__PURE__ */ jsx(IntegrityStatus, {}) }),
|
|
1199
1234
|
/* @__PURE__ */ jsx(
|
|
1200
1235
|
AuditLogFilters,
|
|
1201
1236
|
{
|
|
@@ -1213,8 +1248,7 @@ const AuditLogs = () => {
|
|
|
1213
1248
|
isLoading,
|
|
1214
1249
|
sort: String(query.sort ?? "createdAt:desc"),
|
|
1215
1250
|
onSortChange: (sort) => setQuery({ sort, page: 1 }),
|
|
1216
|
-
onView: (log) => navigate(`${log.id}`)
|
|
1217
|
-
onDelete: canDelete ? setPendingDelete : null
|
|
1251
|
+
onView: (log) => navigate(`${log.id}`)
|
|
1218
1252
|
}
|
|
1219
1253
|
),
|
|
1220
1254
|
/* @__PURE__ */ jsx(Box, { paddingTop: 4, children: /* @__PURE__ */ jsx(
|
|
@@ -1229,31 +1263,7 @@ const AuditLogs = () => {
|
|
|
1229
1263
|
] })
|
|
1230
1264
|
}
|
|
1231
1265
|
) })
|
|
1232
|
-
] })
|
|
1233
|
-
/* @__PURE__ */ jsx(
|
|
1234
|
-
Dialog.Root,
|
|
1235
|
-
{
|
|
1236
|
-
open: pendingDelete !== null,
|
|
1237
|
-
onOpenChange: (open) => {
|
|
1238
|
-
if (!open) setPendingDelete(null);
|
|
1239
|
-
},
|
|
1240
|
-
children: /* @__PURE__ */ jsx(
|
|
1241
|
-
ConfirmDialog,
|
|
1242
|
-
{
|
|
1243
|
-
title: formatMessage({
|
|
1244
|
-
id: getTranslation("delete.title"),
|
|
1245
|
-
defaultMessage: "Delete audit record"
|
|
1246
|
-
}),
|
|
1247
|
-
onConfirm: confirmDelete,
|
|
1248
|
-
onCancel: () => setPendingDelete(null),
|
|
1249
|
-
children: formatMessage({
|
|
1250
|
-
id: getTranslation("delete.confirm"),
|
|
1251
|
-
defaultMessage: "Audit records are evidence of what happened to your content. Deleting one cannot be undone. Are you sure?"
|
|
1252
|
-
})
|
|
1253
|
-
}
|
|
1254
|
-
)
|
|
1255
|
-
}
|
|
1256
|
-
)
|
|
1266
|
+
] })
|
|
1257
1267
|
] });
|
|
1258
1268
|
};
|
|
1259
1269
|
const App = () => /* @__PURE__ */ jsx(Page.Protect, { permissions: PERMISSIONS.read, children: /* @__PURE__ */ jsxs(Routes, { children: [
|
|
@@ -5,9 +5,8 @@ const en = {
|
|
|
5
5
|
"plugin.description": "Track every change to your content and every security event.",
|
|
6
6
|
"list.subtitle": "{total, plural, =0 {No records} one {# record} other {# records}} of content and security activity",
|
|
7
7
|
"list.search": "Search by document id, user, content type or request id",
|
|
8
|
-
"list.empty": "No audit logs match these filters. Records appear here as soon as content is created, updated, deleted, published or unpublished, and whenever someone signs in, signs out, is refused access, or changes an admin user, role, permission or media file.",
|
|
8
|
+
"list.empty": "No audit logs match these filters. Records appear here as soon as content is created, updated, deleted, published or unpublished, and whenever someone signs in, signs out, is refused access, or changes an admin user, role, permission or media file. Records cannot be deleted from this page.",
|
|
9
9
|
"list.view": "View details",
|
|
10
|
-
"list.delete": "Delete this record",
|
|
11
10
|
"list.column.createdAt": "Date",
|
|
12
11
|
"list.column.userName": "User",
|
|
13
12
|
"list.column.action": "Action",
|
|
@@ -53,12 +52,6 @@ const en = {
|
|
|
53
52
|
"changes.empty": "No field-level changes were recorded for this operation. This is expected for creates and deletes, and for updates that re-saved a document without modifying it.",
|
|
54
53
|
"json.show": "Show ({count} lines)",
|
|
55
54
|
"json.hide": "Hide",
|
|
56
|
-
"delete.title": "Delete audit record",
|
|
57
|
-
"delete.confirm": "Audit records are evidence of what happened to your content. Deleting one cannot be undone. Are you sure?",
|
|
58
|
-
"delete.action": "Delete",
|
|
59
|
-
"delete.cancel": "Cancel",
|
|
60
|
-
"delete.success": "Audit record deleted.",
|
|
61
|
-
"delete.error": "Could not delete this audit record.",
|
|
62
55
|
"filters.outcome": "Outcome",
|
|
63
56
|
"detail.outcome": "Outcome",
|
|
64
57
|
"detail.metadata": "Event detail",
|
|
@@ -82,6 +75,15 @@ const en = {
|
|
|
82
75
|
"widgets.status.added": "Added",
|
|
83
76
|
"widgets.status.removed": "Removed",
|
|
84
77
|
"widgets.status.unchanged": "Unchanged",
|
|
85
|
-
"list.column.outcome": "Outcome"
|
|
78
|
+
"list.column.outcome": "Outcome",
|
|
79
|
+
"detail.hash": "Hash",
|
|
80
|
+
"detail.prevHash": "Previous hash",
|
|
81
|
+
"integrity.checking": "Verifying the audit chain…",
|
|
82
|
+
"integrity.unavailable": "Integrity check unavailable: {error}",
|
|
83
|
+
"integrity.ok": "Chain verified",
|
|
84
|
+
"integrity.summary": "{hashed, plural, one {# linked record} other {# linked records}}{legacy, plural, =0 {} one { · # pre-chain record} other { · # pre-chain records}} · checked {when}",
|
|
85
|
+
"integrity.broken": "CHAIN BROKEN",
|
|
86
|
+
"integrity.brokenAt": "Record #{id}: {reason}",
|
|
87
|
+
"integrity.brokenHint": "A record was modified, removed or inserted after it was written. Treat everything from this point as unverified and compare against the forwarded copy."
|
|
86
88
|
};
|
|
87
89
|
exports.default = en;
|
|
@@ -3,9 +3,8 @@ const en = {
|
|
|
3
3
|
"plugin.description": "Track every change to your content and every security event.",
|
|
4
4
|
"list.subtitle": "{total, plural, =0 {No records} one {# record} other {# records}} of content and security activity",
|
|
5
5
|
"list.search": "Search by document id, user, content type or request id",
|
|
6
|
-
"list.empty": "No audit logs match these filters. Records appear here as soon as content is created, updated, deleted, published or unpublished, and whenever someone signs in, signs out, is refused access, or changes an admin user, role, permission or media file.",
|
|
6
|
+
"list.empty": "No audit logs match these filters. Records appear here as soon as content is created, updated, deleted, published or unpublished, and whenever someone signs in, signs out, is refused access, or changes an admin user, role, permission or media file. Records cannot be deleted from this page.",
|
|
7
7
|
"list.view": "View details",
|
|
8
|
-
"list.delete": "Delete this record",
|
|
9
8
|
"list.column.createdAt": "Date",
|
|
10
9
|
"list.column.userName": "User",
|
|
11
10
|
"list.column.action": "Action",
|
|
@@ -51,12 +50,6 @@ const en = {
|
|
|
51
50
|
"changes.empty": "No field-level changes were recorded for this operation. This is expected for creates and deletes, and for updates that re-saved a document without modifying it.",
|
|
52
51
|
"json.show": "Show ({count} lines)",
|
|
53
52
|
"json.hide": "Hide",
|
|
54
|
-
"delete.title": "Delete audit record",
|
|
55
|
-
"delete.confirm": "Audit records are evidence of what happened to your content. Deleting one cannot be undone. Are you sure?",
|
|
56
|
-
"delete.action": "Delete",
|
|
57
|
-
"delete.cancel": "Cancel",
|
|
58
|
-
"delete.success": "Audit record deleted.",
|
|
59
|
-
"delete.error": "Could not delete this audit record.",
|
|
60
53
|
"filters.outcome": "Outcome",
|
|
61
54
|
"detail.outcome": "Outcome",
|
|
62
55
|
"detail.metadata": "Event detail",
|
|
@@ -80,7 +73,16 @@ const en = {
|
|
|
80
73
|
"widgets.status.added": "Added",
|
|
81
74
|
"widgets.status.removed": "Removed",
|
|
82
75
|
"widgets.status.unchanged": "Unchanged",
|
|
83
|
-
"list.column.outcome": "Outcome"
|
|
76
|
+
"list.column.outcome": "Outcome",
|
|
77
|
+
"detail.hash": "Hash",
|
|
78
|
+
"detail.prevHash": "Previous hash",
|
|
79
|
+
"integrity.checking": "Verifying the audit chain…",
|
|
80
|
+
"integrity.unavailable": "Integrity check unavailable: {error}",
|
|
81
|
+
"integrity.ok": "Chain verified",
|
|
82
|
+
"integrity.summary": "{hashed, plural, one {# linked record} other {# linked records}}{legacy, plural, =0 {} one { · # pre-chain record} other { · # pre-chain records}} · checked {when}",
|
|
83
|
+
"integrity.broken": "CHAIN BROKEN",
|
|
84
|
+
"integrity.brokenAt": "Record #{id}: {reason}",
|
|
85
|
+
"integrity.brokenHint": "A record was modified, removed or inserted after it was written. Treat everything from this point as unverified and compare against the forwarded copy."
|
|
84
86
|
};
|
|
85
87
|
export {
|
|
86
88
|
en as default
|
|
@@ -27,9 +27,7 @@ const entity = (action) => ({
|
|
|
27
27
|
conditions: []
|
|
28
28
|
});
|
|
29
29
|
const PERMISSIONS = {
|
|
30
|
-
read: [entity(`plugin::${PLUGIN_ID}.read`)]
|
|
31
|
-
delete: [entity(`plugin::${PLUGIN_ID}.delete`)],
|
|
32
|
-
settings: [entity(`plugin::${PLUGIN_ID}.settings`)]
|
|
30
|
+
read: [entity(`plugin::${PLUGIN_ID}.read`)]
|
|
33
31
|
};
|
|
34
32
|
const getTranslation = (id) => `${PLUGIN_ID}.${id}`;
|
|
35
33
|
const plugin = {
|
|
@@ -42,7 +40,7 @@ const plugin = {
|
|
|
42
40
|
defaultMessage: "Audit Logs"
|
|
43
41
|
},
|
|
44
42
|
permissions: PERMISSIONS.read,
|
|
45
|
-
Component: () => import("./App-
|
|
43
|
+
Component: () => import("./App-jWvaMXIw.mjs").then((mod) => ({ default: mod.App }))
|
|
46
44
|
});
|
|
47
45
|
app.registerPlugin({
|
|
48
46
|
id: PLUGIN_ID,
|
|
@@ -60,7 +58,7 @@ const plugin = {
|
|
|
60
58
|
return Promise.all(
|
|
61
59
|
locales.map(async (locale) => {
|
|
62
60
|
try {
|
|
63
|
-
const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("./en-
|
|
61
|
+
const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("./en-C3Og1VyK.mjs") }), `./translations/${locale}.json`, 3);
|
|
64
62
|
const namespaced = {};
|
|
65
63
|
for (const key of Object.keys(data)) {
|
|
66
64
|
namespaced[getTranslation(key)] = data[key];
|
|
@@ -28,9 +28,7 @@ const entity = (action) => ({
|
|
|
28
28
|
conditions: []
|
|
29
29
|
});
|
|
30
30
|
const PERMISSIONS = {
|
|
31
|
-
read: [entity(`plugin::${PLUGIN_ID}.read`)]
|
|
32
|
-
delete: [entity(`plugin::${PLUGIN_ID}.delete`)],
|
|
33
|
-
settings: [entity(`plugin::${PLUGIN_ID}.settings`)]
|
|
31
|
+
read: [entity(`plugin::${PLUGIN_ID}.read`)]
|
|
34
32
|
};
|
|
35
33
|
const getTranslation = (id) => `${PLUGIN_ID}.${id}`;
|
|
36
34
|
const plugin = {
|
|
@@ -43,7 +41,7 @@ const plugin = {
|
|
|
43
41
|
defaultMessage: "Audit Logs"
|
|
44
42
|
},
|
|
45
43
|
permissions: PERMISSIONS.read,
|
|
46
|
-
Component: () => Promise.resolve().then(() => require("./App-
|
|
44
|
+
Component: () => Promise.resolve().then(() => require("./App-CmbG2k9E.js")).then((mod) => ({ default: mod.App }))
|
|
47
45
|
});
|
|
48
46
|
app.registerPlugin({
|
|
49
47
|
id: PLUGIN_ID,
|
|
@@ -61,7 +59,7 @@ const plugin = {
|
|
|
61
59
|
return Promise.all(
|
|
62
60
|
locales.map(async (locale) => {
|
|
63
61
|
try {
|
|
64
|
-
const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => Promise.resolve().then(() => require("./en-
|
|
62
|
+
const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => Promise.resolve().then(() => require("./en-Bs4tlklI.js")) }), `./translations/${locale}.json`, 3);
|
|
65
63
|
const namespaced = {};
|
|
66
64
|
for (const key of Object.keys(data)) {
|
|
67
65
|
namespaced[getTranslation(key)] = data[key];
|
package/dist/admin/index.js
CHANGED
package/dist/admin/index.mjs
CHANGED
|
@@ -5,7 +5,6 @@ interface AuditLogTableProps {
|
|
|
5
5
|
sort: string;
|
|
6
6
|
onSortChange: (sort: string) => void;
|
|
7
7
|
onView: (log: AuditLog) => void;
|
|
8
|
-
onDelete: ((log: AuditLog) => void) | null;
|
|
9
8
|
isLoading: boolean;
|
|
10
9
|
}
|
|
11
10
|
/**
|
|
@@ -15,5 +14,5 @@ interface AuditLogTableProps {
|
|
|
15
14
|
* `Array.sort`: the table only ever holds one page, so sorting it locally would
|
|
16
15
|
* reorder twenty rows and quietly lie about the other ten thousand.
|
|
17
16
|
*/
|
|
18
|
-
declare const AuditLogTable: ({ logs, sort, onSortChange, onView,
|
|
17
|
+
declare const AuditLogTable: ({ logs, sort, onSortChange, onView, isLoading, }: AuditLogTableProps) => React.JSX.Element;
|
|
19
18
|
export { AuditLogTable };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hash-chain verdict, shown above the list.
|
|
3
|
+
*
|
|
4
|
+
* An audit log that cannot say whether it has been tampered with is taking its
|
|
5
|
+
* own word for it. This runs the full chain walk on every visit to the page —
|
|
6
|
+
* deliberately, rather than on demand, so that the check is not something an
|
|
7
|
+
* administrator has to remember to do — and states the result in one line.
|
|
8
|
+
*
|
|
9
|
+
* A broken chain is the one thing on this page that should look alarming, so it
|
|
10
|
+
* is the only thing here rendered in red.
|
|
11
|
+
*/
|
|
12
|
+
declare const IntegrityStatus: () => import("react").JSX.Element;
|
|
13
|
+
export { IntegrityStatus };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AuditFilterOptions, AuditLog, AuditPagination } from '../types';
|
|
1
|
+
import { AuditFilterOptions, AuditLog, AuditPagination, IntegrityReport } from '../types';
|
|
2
2
|
/**
|
|
3
3
|
* Fetches one page of audit logs.
|
|
4
4
|
*
|
|
@@ -26,11 +26,22 @@ export declare const useAuditFilterOptions: () => {
|
|
|
26
26
|
options: AuditFilterOptions;
|
|
27
27
|
isLoading: boolean;
|
|
28
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* Runs the hash-chain verification.
|
|
31
|
+
*
|
|
32
|
+
* Fetched once per mount of the list page. The walk is server-side and paged,
|
|
33
|
+
* so its cost grows with the table, but it is a sequential read of an indexed
|
|
34
|
+
* primary key — the kind of thing a database does quickly — and the check is
|
|
35
|
+
* worth nothing if it is not run.
|
|
36
|
+
*/
|
|
37
|
+
export declare const useAuditIntegrity: () => {
|
|
38
|
+
data: IntegrityReport | null;
|
|
39
|
+
isLoading: boolean;
|
|
40
|
+
error: string | null;
|
|
41
|
+
};
|
|
29
42
|
/** Loads one audit record for the detail page. */
|
|
30
43
|
export declare const useAuditLog: (id: string | undefined) => {
|
|
31
44
|
data: AuditLog | null;
|
|
32
45
|
isLoading: boolean;
|
|
33
46
|
error: string | null;
|
|
34
47
|
};
|
|
35
|
-
/** Deletes one record. Requires `plugin::audit-log.delete` on the server. */
|
|
36
|
-
export declare const useDeleteAuditLog: () => (id: number) => Promise<void>;
|
|
@@ -7,14 +7,6 @@ export declare const PERMISSIONS: {
|
|
|
7
7
|
properties: {};
|
|
8
8
|
conditions: never[];
|
|
9
9
|
}[];
|
|
10
|
-
delete: {
|
|
11
|
-
action: string;
|
|
12
|
-
subject: null;
|
|
13
|
-
id: string;
|
|
14
|
-
actionParameters: {};
|
|
15
|
-
properties: {};
|
|
16
|
-
conditions: never[];
|
|
17
|
-
}[];
|
|
18
10
|
settings: {
|
|
19
11
|
action: string;
|
|
20
12
|
subject: null;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* own model: `Date` columns are ISO strings here, and the compiled ignore-list
|
|
6
6
|
* matchers on `AuditConfig` never cross the wire. The canonical definitions live
|
|
7
7
|
* in `server/src/types` and are re-exported from
|
|
8
|
-
* `strapi-
|
|
8
|
+
* `strapi-cms-audit-log/strapi-server`; see the note there on why the two
|
|
9
9
|
* halves cannot share one module.
|
|
10
10
|
*/
|
|
11
11
|
export type AuditAction = 'create' | 'update' | 'delete' | 'publish' | 'unpublish';
|
|
@@ -15,7 +15,9 @@ export type AuditAction = 'create' | 'update' | 'delete' | 'publish' | 'unpublis
|
|
|
15
15
|
* govern everything else.
|
|
16
16
|
*/
|
|
17
17
|
export type AuditSecurityAction = 'login.success' | 'login.failed' | 'logout' | 'access.denied' | 'admin.user.create' | 'admin.user.update' | 'admin.user.delete' | 'admin.role.create' | 'admin.role.update' | 'admin.role.delete' | 'admin.permission.create' | 'admin.permission.update' | 'admin.permission.delete' | 'media.create' | 'media.update' | 'media.delete' | 'media-folder.create' | 'media-folder.update' | 'media-folder.delete';
|
|
18
|
-
|
|
18
|
+
/** The audit log's own maintenance: the retention purge recording itself. */
|
|
19
|
+
export type AuditMaintenanceAction = 'retention.purge';
|
|
20
|
+
export type AuditAnyAction = AuditAction | AuditSecurityAction | AuditMaintenanceAction;
|
|
19
21
|
/** Whether the recorded attempt succeeded. Only security actions are ever `failure`. */
|
|
20
22
|
export type AuditOutcome = 'success' | 'failure';
|
|
21
23
|
export type AuditSource = 'admin' | 'api' | 'system' | 'cron' | 'migration' | 'unknown';
|
|
@@ -46,9 +48,31 @@ export interface AuditLog {
|
|
|
46
48
|
outcome: AuditOutcome | null;
|
|
47
49
|
/** Action-specific detail: the reason a login failed, the path of a denied request, an upload's filename. */
|
|
48
50
|
metadata: Record<string, unknown> | null;
|
|
51
|
+
/** SHA-256 of this record and its predecessor's hash. Null on rows written before the chain existed. */
|
|
52
|
+
hash: string | null;
|
|
53
|
+
prevHash: string | null;
|
|
49
54
|
createdAt: string;
|
|
50
55
|
updatedAt: string;
|
|
51
56
|
}
|
|
57
|
+
/** Verdict of a full walk of the hash chain. Mirrors the server's `IntegrityReport`. */
|
|
58
|
+
export interface IntegrityReport {
|
|
59
|
+
ok: boolean;
|
|
60
|
+
checked: number;
|
|
61
|
+
hashed: number;
|
|
62
|
+
legacy: number;
|
|
63
|
+
head: {
|
|
64
|
+
id: number;
|
|
65
|
+
hash: string;
|
|
66
|
+
} | null;
|
|
67
|
+
start: {
|
|
68
|
+
id: number;
|
|
69
|
+
} | null;
|
|
70
|
+
brokenAt?: {
|
|
71
|
+
id: number;
|
|
72
|
+
reason: string;
|
|
73
|
+
};
|
|
74
|
+
verifiedAt: string;
|
|
75
|
+
}
|
|
52
76
|
export interface AuditPagination {
|
|
53
77
|
page: number;
|
|
54
78
|
pageSize: number;
|