ニジカ投稿局 https://tv.nizika.tv
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

audit-logger.ts 6.8 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. import {
  2. AdminAbuse,
  3. CustomConfig,
  4. User,
  5. VideoChannel,
  6. VideoChannelSync,
  7. VideoComment,
  8. VideoDetails,
  9. VideoImport
  10. } from '@peertube/peertube-models'
  11. import { AUDIT_LOG_FILENAME } from '@server/initializers/constants.js'
  12. import { diff } from 'deep-object-diff'
  13. import express from 'express'
  14. import { flatten } from 'flat'
  15. import { join } from 'path'
  16. import { addColors, config, createLogger, format, transports } from 'winston'
  17. import { CONFIG } from '../initializers/config.js'
  18. import { jsonLoggerFormat, labelFormatter } from './logger.js'
  19. function getAuditIdFromRes (res: express.Response) {
  20. return res.locals.oauth.token.User.username
  21. }
  22. enum AUDIT_TYPE {
  23. CREATE = 'create',
  24. UPDATE = 'update',
  25. DELETE = 'delete'
  26. }
  27. const colors = config.npm.colors
  28. colors.audit = config.npm.colors.info
  29. addColors(colors)
  30. const auditLogger = createLogger({
  31. levels: { audit: 0 },
  32. transports: [
  33. new transports.File({
  34. filename: join(CONFIG.STORAGE.LOG_DIR, AUDIT_LOG_FILENAME),
  35. level: 'audit',
  36. maxsize: 5242880,
  37. maxFiles: 5,
  38. format: format.combine(
  39. format.timestamp(),
  40. labelFormatter(),
  41. format.splat(),
  42. jsonLoggerFormat
  43. )
  44. })
  45. ],
  46. exitOnError: true
  47. })
  48. function auditLoggerWrapper (domain: string, user: string, action: AUDIT_TYPE, entity: EntityAuditView, oldEntity: EntityAuditView = null) {
  49. let entityInfos: object
  50. if (action === AUDIT_TYPE.UPDATE && oldEntity) {
  51. const oldEntityKeys = oldEntity.toLogKeys()
  52. const diffObject = diff(oldEntityKeys, entity.toLogKeys())
  53. const diffKeys = Object.entries(diffObject).reduce((newKeys, entry) => {
  54. newKeys[`new-${entry[0]}`] = entry[1]
  55. return newKeys
  56. }, {})
  57. entityInfos = { ...oldEntityKeys, ...diffKeys }
  58. } else {
  59. entityInfos = { ...entity.toLogKeys() }
  60. }
  61. auditLogger.log('audit', JSON.stringify({
  62. user,
  63. domain,
  64. action,
  65. ...entityInfos
  66. }))
  67. }
  68. function auditLoggerFactory (domain: string) {
  69. return {
  70. create (user: string, entity: EntityAuditView) {
  71. auditLoggerWrapper(domain, user, AUDIT_TYPE.CREATE, entity)
  72. },
  73. update (user: string, entity: EntityAuditView, oldEntity: EntityAuditView) {
  74. auditLoggerWrapper(domain, user, AUDIT_TYPE.UPDATE, entity, oldEntity)
  75. },
  76. delete (user: string, entity: EntityAuditView) {
  77. auditLoggerWrapper(domain, user, AUDIT_TYPE.DELETE, entity)
  78. }
  79. }
  80. }
  81. abstract class EntityAuditView {
  82. constructor (private readonly keysToKeep: Set<string>, private readonly prefix: string, private readonly entityInfos: object) { }
  83. toLogKeys (): object {
  84. const obj = flatten<object, any>(this.entityInfos, { delimiter: '-', safe: true })
  85. return Object.keys(obj)
  86. .filter(key => this.keysToKeep.has(key))
  87. .reduce((p, k) => ({ ...p, [`${this.prefix}-${k}`]: obj[k] }), {})
  88. }
  89. }
  90. const videoKeysToKeep = new Set([
  91. 'tags',
  92. 'uuid',
  93. 'id',
  94. 'uuid',
  95. 'createdAt',
  96. 'updatedAt',
  97. 'publishedAt',
  98. 'category',
  99. 'licence',
  100. 'language',
  101. 'privacy',
  102. 'description',
  103. 'duration',
  104. 'isLocal',
  105. 'name',
  106. 'thumbnailPath',
  107. 'previewPath',
  108. 'nsfw',
  109. 'waitTranscoding',
  110. 'account-id',
  111. 'account-uuid',
  112. 'account-name',
  113. 'channel-id',
  114. 'channel-uuid',
  115. 'channel-name',
  116. 'support',
  117. 'commentsPolicy',
  118. 'downloadEnabled'
  119. ])
  120. class VideoAuditView extends EntityAuditView {
  121. constructor (video: VideoDetails) {
  122. super(videoKeysToKeep, 'video', video)
  123. }
  124. }
  125. const videoImportKeysToKeep = new Set([
  126. 'id',
  127. 'targetUrl',
  128. 'video-name'
  129. ])
  130. class VideoImportAuditView extends EntityAuditView {
  131. constructor (videoImport: VideoImport) {
  132. super(videoImportKeysToKeep, 'video-import', videoImport)
  133. }
  134. }
  135. const commentKeysToKeep = new Set([
  136. 'id',
  137. 'text',
  138. 'threadId',
  139. 'inReplyToCommentId',
  140. 'videoId',
  141. 'createdAt',
  142. 'updatedAt',
  143. 'totalReplies',
  144. 'account-id',
  145. 'account-uuid',
  146. 'account-name'
  147. ])
  148. class CommentAuditView extends EntityAuditView {
  149. constructor (comment: VideoComment) {
  150. super(commentKeysToKeep, 'comment', comment)
  151. }
  152. }
  153. const userKeysToKeep = new Set([
  154. 'id',
  155. 'username',
  156. 'email',
  157. 'nsfwPolicy',
  158. 'autoPlayVideo',
  159. 'role',
  160. 'videoQuota',
  161. 'createdAt',
  162. 'account-id',
  163. 'account-uuid',
  164. 'account-name',
  165. 'account-followingCount',
  166. 'account-followersCount',
  167. 'account-createdAt',
  168. 'account-updatedAt',
  169. 'account-avatar-path',
  170. 'account-avatar-createdAt',
  171. 'account-avatar-updatedAt',
  172. 'account-displayName',
  173. 'account-description',
  174. 'videoChannels'
  175. ])
  176. class UserAuditView extends EntityAuditView {
  177. constructor (user: User) {
  178. super(userKeysToKeep, 'user', user)
  179. }
  180. }
  181. const channelKeysToKeep = new Set([
  182. 'id',
  183. 'uuid',
  184. 'name',
  185. 'followingCount',
  186. 'followersCount',
  187. 'createdAt',
  188. 'updatedAt',
  189. 'avatar-path',
  190. 'avatar-createdAt',
  191. 'avatar-updatedAt',
  192. 'displayName',
  193. 'description',
  194. 'support',
  195. 'isLocal',
  196. 'ownerAccount-id',
  197. 'ownerAccount-uuid',
  198. 'ownerAccount-name',
  199. 'ownerAccount-displayedName'
  200. ])
  201. class VideoChannelAuditView extends EntityAuditView {
  202. constructor (channel: VideoChannel) {
  203. super(channelKeysToKeep, 'channel', channel)
  204. }
  205. }
  206. const abuseKeysToKeep = new Set([
  207. 'id',
  208. 'reason',
  209. 'reporterAccount',
  210. 'createdAt'
  211. ])
  212. class AbuseAuditView extends EntityAuditView {
  213. constructor (abuse: AdminAbuse) {
  214. super(abuseKeysToKeep, 'abuse', abuse)
  215. }
  216. }
  217. const customConfigKeysToKeep = new Set([
  218. 'instance-name',
  219. 'instance-shortDescription',
  220. 'instance-description',
  221. 'instance-terms',
  222. 'instance-defaultClientRoute',
  223. 'instance-defaultNSFWPolicy',
  224. 'instance-customizations-javascript',
  225. 'instance-customizations-css',
  226. 'services-twitter-username',
  227. 'cache-previews-size',
  228. 'cache-captions-size',
  229. 'signup-enabled',
  230. 'signup-limit',
  231. 'signup-requiresEmailVerification',
  232. 'admin-email',
  233. 'user-videoQuota',
  234. 'transcoding-enabled',
  235. 'transcoding-threads',
  236. 'transcoding-resolutions'
  237. ])
  238. class CustomConfigAuditView extends EntityAuditView {
  239. constructor (customConfig: CustomConfig) {
  240. const infos: any = customConfig
  241. const resolutionsDict = infos.transcoding.resolutions
  242. const resolutionsArray = []
  243. Object.entries(resolutionsDict)
  244. .forEach(([ resolution, isEnabled ]) => {
  245. if (isEnabled) resolutionsArray.push(resolution)
  246. })
  247. Object.assign({}, infos, { transcoding: { resolutions: resolutionsArray } })
  248. super(customConfigKeysToKeep, 'config', infos)
  249. }
  250. }
  251. const channelSyncKeysToKeep = new Set([
  252. 'id',
  253. 'externalChannelUrl',
  254. 'channel-id',
  255. 'channel-name'
  256. ])
  257. class VideoChannelSyncAuditView extends EntityAuditView {
  258. constructor (channelSync: VideoChannelSync) {
  259. super(channelSyncKeysToKeep, 'channelSync', channelSync)
  260. }
  261. }
  262. export {
  263. getAuditIdFromRes,
  264. auditLoggerFactory,
  265. VideoImportAuditView,
  266. VideoChannelAuditView,
  267. CommentAuditView,
  268. UserAuditView,
  269. VideoAuditView,
  270. AbuseAuditView,
  271. CustomConfigAuditView,
  272. VideoChannelSyncAuditView
  273. }