@etcsec-com/etc-collector 1.4.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 (617) hide show
  1. package/.env.example +60 -0
  2. package/.env.test.example +33 -0
  3. package/.github/workflows/ci.yml +83 -0
  4. package/.github/workflows/release.yml +246 -0
  5. package/.prettierrc.json +10 -0
  6. package/CHANGELOG.md +15 -0
  7. package/Dockerfile +57 -0
  8. package/LICENSE +190 -0
  9. package/README.md +194 -0
  10. package/dist/api/controllers/audit.controller.d.ts +21 -0
  11. package/dist/api/controllers/audit.controller.d.ts.map +1 -0
  12. package/dist/api/controllers/audit.controller.js +179 -0
  13. package/dist/api/controllers/audit.controller.js.map +1 -0
  14. package/dist/api/controllers/auth.controller.d.ts +16 -0
  15. package/dist/api/controllers/auth.controller.d.ts.map +1 -0
  16. package/dist/api/controllers/auth.controller.js +146 -0
  17. package/dist/api/controllers/auth.controller.js.map +1 -0
  18. package/dist/api/controllers/export.controller.d.ts +27 -0
  19. package/dist/api/controllers/export.controller.d.ts.map +1 -0
  20. package/dist/api/controllers/export.controller.js +80 -0
  21. package/dist/api/controllers/export.controller.js.map +1 -0
  22. package/dist/api/controllers/health.controller.d.ts +5 -0
  23. package/dist/api/controllers/health.controller.d.ts.map +1 -0
  24. package/dist/api/controllers/health.controller.js +16 -0
  25. package/dist/api/controllers/health.controller.js.map +1 -0
  26. package/dist/api/controllers/jobs.controller.d.ts +13 -0
  27. package/dist/api/controllers/jobs.controller.d.ts.map +1 -0
  28. package/dist/api/controllers/jobs.controller.js +125 -0
  29. package/dist/api/controllers/jobs.controller.js.map +1 -0
  30. package/dist/api/controllers/providers.controller.d.ts +15 -0
  31. package/dist/api/controllers/providers.controller.d.ts.map +1 -0
  32. package/dist/api/controllers/providers.controller.js +112 -0
  33. package/dist/api/controllers/providers.controller.js.map +1 -0
  34. package/dist/api/dto/AuditRequest.dto.d.ts +6 -0
  35. package/dist/api/dto/AuditRequest.dto.d.ts.map +1 -0
  36. package/dist/api/dto/AuditRequest.dto.js +3 -0
  37. package/dist/api/dto/AuditRequest.dto.js.map +1 -0
  38. package/dist/api/dto/AuditResponse.dto.d.ts +17 -0
  39. package/dist/api/dto/AuditResponse.dto.d.ts.map +1 -0
  40. package/dist/api/dto/AuditResponse.dto.js +3 -0
  41. package/dist/api/dto/AuditResponse.dto.js.map +1 -0
  42. package/dist/api/dto/TokenRequest.dto.d.ts +6 -0
  43. package/dist/api/dto/TokenRequest.dto.d.ts.map +1 -0
  44. package/dist/api/dto/TokenRequest.dto.js +3 -0
  45. package/dist/api/dto/TokenRequest.dto.js.map +1 -0
  46. package/dist/api/dto/TokenResponse.dto.d.ts +12 -0
  47. package/dist/api/dto/TokenResponse.dto.d.ts.map +1 -0
  48. package/dist/api/dto/TokenResponse.dto.js +3 -0
  49. package/dist/api/dto/TokenResponse.dto.js.map +1 -0
  50. package/dist/api/middlewares/authenticate.d.ts +12 -0
  51. package/dist/api/middlewares/authenticate.d.ts.map +1 -0
  52. package/dist/api/middlewares/authenticate.js +141 -0
  53. package/dist/api/middlewares/authenticate.js.map +1 -0
  54. package/dist/api/middlewares/errorHandler.d.ts +3 -0
  55. package/dist/api/middlewares/errorHandler.d.ts.map +1 -0
  56. package/dist/api/middlewares/errorHandler.js +30 -0
  57. package/dist/api/middlewares/errorHandler.js.map +1 -0
  58. package/dist/api/middlewares/rateLimit.d.ts +3 -0
  59. package/dist/api/middlewares/rateLimit.d.ts.map +1 -0
  60. package/dist/api/middlewares/rateLimit.js +34 -0
  61. package/dist/api/middlewares/rateLimit.js.map +1 -0
  62. package/dist/api/middlewares/validate.d.ts +4 -0
  63. package/dist/api/middlewares/validate.d.ts.map +1 -0
  64. package/dist/api/middlewares/validate.js +31 -0
  65. package/dist/api/middlewares/validate.js.map +1 -0
  66. package/dist/api/routes/audit.routes.d.ts +5 -0
  67. package/dist/api/routes/audit.routes.d.ts.map +1 -0
  68. package/dist/api/routes/audit.routes.js +24 -0
  69. package/dist/api/routes/audit.routes.js.map +1 -0
  70. package/dist/api/routes/auth.routes.d.ts +6 -0
  71. package/dist/api/routes/auth.routes.d.ts.map +1 -0
  72. package/dist/api/routes/auth.routes.js +22 -0
  73. package/dist/api/routes/auth.routes.js.map +1 -0
  74. package/dist/api/routes/export.routes.d.ts +5 -0
  75. package/dist/api/routes/export.routes.d.ts.map +1 -0
  76. package/dist/api/routes/export.routes.js +16 -0
  77. package/dist/api/routes/export.routes.js.map +1 -0
  78. package/dist/api/routes/health.routes.d.ts +4 -0
  79. package/dist/api/routes/health.routes.d.ts.map +1 -0
  80. package/dist/api/routes/health.routes.js +11 -0
  81. package/dist/api/routes/health.routes.js.map +1 -0
  82. package/dist/api/routes/index.d.ts +10 -0
  83. package/dist/api/routes/index.d.ts.map +1 -0
  84. package/dist/api/routes/index.js +20 -0
  85. package/dist/api/routes/index.js.map +1 -0
  86. package/dist/api/routes/providers.routes.d.ts +5 -0
  87. package/dist/api/routes/providers.routes.d.ts.map +1 -0
  88. package/dist/api/routes/providers.routes.js +13 -0
  89. package/dist/api/routes/providers.routes.js.map +1 -0
  90. package/dist/api/validators/audit.schemas.d.ts +60 -0
  91. package/dist/api/validators/audit.schemas.d.ts.map +1 -0
  92. package/dist/api/validators/audit.schemas.js +55 -0
  93. package/dist/api/validators/audit.schemas.js.map +1 -0
  94. package/dist/api/validators/auth.schemas.d.ts +17 -0
  95. package/dist/api/validators/auth.schemas.d.ts.map +1 -0
  96. package/dist/api/validators/auth.schemas.js +21 -0
  97. package/dist/api/validators/auth.schemas.js.map +1 -0
  98. package/dist/app.d.ts +3 -0
  99. package/dist/app.d.ts.map +1 -0
  100. package/dist/app.js +62 -0
  101. package/dist/app.js.map +1 -0
  102. package/dist/config/config.schema.d.ts +65 -0
  103. package/dist/config/config.schema.d.ts.map +1 -0
  104. package/dist/config/config.schema.js +95 -0
  105. package/dist/config/config.schema.js.map +1 -0
  106. package/dist/config/index.d.ts +4 -0
  107. package/dist/config/index.d.ts.map +1 -0
  108. package/dist/config/index.js +75 -0
  109. package/dist/config/index.js.map +1 -0
  110. package/dist/container.d.ts +47 -0
  111. package/dist/container.d.ts.map +1 -0
  112. package/dist/container.js +137 -0
  113. package/dist/container.js.map +1 -0
  114. package/dist/data/database.d.ts +13 -0
  115. package/dist/data/database.d.ts.map +1 -0
  116. package/dist/data/database.js +68 -0
  117. package/dist/data/database.js.map +1 -0
  118. package/dist/data/jobs/token-cleanup.job.d.ts +23 -0
  119. package/dist/data/jobs/token-cleanup.job.d.ts.map +1 -0
  120. package/dist/data/jobs/token-cleanup.job.js +96 -0
  121. package/dist/data/jobs/token-cleanup.job.js.map +1 -0
  122. package/dist/data/migrations/migration.runner.d.ts +13 -0
  123. package/dist/data/migrations/migration.runner.d.ts.map +1 -0
  124. package/dist/data/migrations/migration.runner.js +136 -0
  125. package/dist/data/migrations/migration.runner.js.map +1 -0
  126. package/dist/data/models/Token.model.d.ts +30 -0
  127. package/dist/data/models/Token.model.d.ts.map +1 -0
  128. package/dist/data/models/Token.model.js +3 -0
  129. package/dist/data/models/Token.model.js.map +1 -0
  130. package/dist/data/repositories/token.repository.d.ts +16 -0
  131. package/dist/data/repositories/token.repository.d.ts.map +1 -0
  132. package/dist/data/repositories/token.repository.js +97 -0
  133. package/dist/data/repositories/token.repository.js.map +1 -0
  134. package/dist/providers/azure/auth.provider.d.ts +5 -0
  135. package/dist/providers/azure/auth.provider.d.ts.map +1 -0
  136. package/dist/providers/azure/auth.provider.js +13 -0
  137. package/dist/providers/azure/auth.provider.js.map +1 -0
  138. package/dist/providers/azure/azure-errors.d.ts +40 -0
  139. package/dist/providers/azure/azure-errors.d.ts.map +1 -0
  140. package/dist/providers/azure/azure-errors.js +121 -0
  141. package/dist/providers/azure/azure-errors.js.map +1 -0
  142. package/dist/providers/azure/azure-retry.d.ts +41 -0
  143. package/dist/providers/azure/azure-retry.d.ts.map +1 -0
  144. package/dist/providers/azure/azure-retry.js +85 -0
  145. package/dist/providers/azure/azure-retry.js.map +1 -0
  146. package/dist/providers/azure/graph-client.d.ts +26 -0
  147. package/dist/providers/azure/graph-client.d.ts.map +1 -0
  148. package/dist/providers/azure/graph-client.js +146 -0
  149. package/dist/providers/azure/graph-client.js.map +1 -0
  150. package/dist/providers/azure/graph.provider.d.ts +23 -0
  151. package/dist/providers/azure/graph.provider.d.ts.map +1 -0
  152. package/dist/providers/azure/graph.provider.js +161 -0
  153. package/dist/providers/azure/graph.provider.js.map +1 -0
  154. package/dist/providers/azure/queries/app.queries.d.ts +6 -0
  155. package/dist/providers/azure/queries/app.queries.d.ts.map +1 -0
  156. package/dist/providers/azure/queries/app.queries.js +9 -0
  157. package/dist/providers/azure/queries/app.queries.js.map +1 -0
  158. package/dist/providers/azure/queries/policy.queries.d.ts +6 -0
  159. package/dist/providers/azure/queries/policy.queries.d.ts.map +1 -0
  160. package/dist/providers/azure/queries/policy.queries.js +9 -0
  161. package/dist/providers/azure/queries/policy.queries.js.map +1 -0
  162. package/dist/providers/azure/queries/user.queries.d.ts +7 -0
  163. package/dist/providers/azure/queries/user.queries.d.ts.map +1 -0
  164. package/dist/providers/azure/queries/user.queries.js +10 -0
  165. package/dist/providers/azure/queries/user.queries.js.map +1 -0
  166. package/dist/providers/interfaces/IGraphProvider.d.ts +31 -0
  167. package/dist/providers/interfaces/IGraphProvider.d.ts.map +1 -0
  168. package/dist/providers/interfaces/IGraphProvider.js +3 -0
  169. package/dist/providers/interfaces/IGraphProvider.js.map +1 -0
  170. package/dist/providers/interfaces/ILDAPProvider.d.ts +37 -0
  171. package/dist/providers/interfaces/ILDAPProvider.d.ts.map +1 -0
  172. package/dist/providers/interfaces/ILDAPProvider.js +3 -0
  173. package/dist/providers/interfaces/ILDAPProvider.js.map +1 -0
  174. package/dist/providers/ldap/acl-parser.d.ts +8 -0
  175. package/dist/providers/ldap/acl-parser.d.ts.map +1 -0
  176. package/dist/providers/ldap/acl-parser.js +157 -0
  177. package/dist/providers/ldap/acl-parser.js.map +1 -0
  178. package/dist/providers/ldap/ad-mappers.d.ts +8 -0
  179. package/dist/providers/ldap/ad-mappers.d.ts.map +1 -0
  180. package/dist/providers/ldap/ad-mappers.js +162 -0
  181. package/dist/providers/ldap/ad-mappers.js.map +1 -0
  182. package/dist/providers/ldap/ldap-client.d.ts +33 -0
  183. package/dist/providers/ldap/ldap-client.d.ts.map +1 -0
  184. package/dist/providers/ldap/ldap-client.js +195 -0
  185. package/dist/providers/ldap/ldap-client.js.map +1 -0
  186. package/dist/providers/ldap/ldap-errors.d.ts +48 -0
  187. package/dist/providers/ldap/ldap-errors.d.ts.map +1 -0
  188. package/dist/providers/ldap/ldap-errors.js +120 -0
  189. package/dist/providers/ldap/ldap-errors.js.map +1 -0
  190. package/dist/providers/ldap/ldap-retry.d.ts +14 -0
  191. package/dist/providers/ldap/ldap-retry.d.ts.map +1 -0
  192. package/dist/providers/ldap/ldap-retry.js +102 -0
  193. package/dist/providers/ldap/ldap-retry.js.map +1 -0
  194. package/dist/providers/ldap/ldap-sanitizer.d.ts +12 -0
  195. package/dist/providers/ldap/ldap-sanitizer.d.ts.map +1 -0
  196. package/dist/providers/ldap/ldap-sanitizer.js +104 -0
  197. package/dist/providers/ldap/ldap-sanitizer.js.map +1 -0
  198. package/dist/providers/ldap/ldap.provider.d.ts +21 -0
  199. package/dist/providers/ldap/ldap.provider.d.ts.map +1 -0
  200. package/dist/providers/ldap/ldap.provider.js +165 -0
  201. package/dist/providers/ldap/ldap.provider.js.map +1 -0
  202. package/dist/providers/ldap/queries/computer.queries.d.ts +6 -0
  203. package/dist/providers/ldap/queries/computer.queries.d.ts.map +1 -0
  204. package/dist/providers/ldap/queries/computer.queries.js +9 -0
  205. package/dist/providers/ldap/queries/computer.queries.js.map +1 -0
  206. package/dist/providers/ldap/queries/group.queries.d.ts +6 -0
  207. package/dist/providers/ldap/queries/group.queries.d.ts.map +1 -0
  208. package/dist/providers/ldap/queries/group.queries.js +9 -0
  209. package/dist/providers/ldap/queries/group.queries.js.map +1 -0
  210. package/dist/providers/ldap/queries/user.queries.d.ts +7 -0
  211. package/dist/providers/ldap/queries/user.queries.d.ts.map +1 -0
  212. package/dist/providers/ldap/queries/user.queries.js +10 -0
  213. package/dist/providers/ldap/queries/user.queries.js.map +1 -0
  214. package/dist/providers/smb/smb.provider.d.ts +68 -0
  215. package/dist/providers/smb/smb.provider.d.ts.map +1 -0
  216. package/dist/providers/smb/smb.provider.js +382 -0
  217. package/dist/providers/smb/smb.provider.js.map +1 -0
  218. package/dist/server.d.ts +2 -0
  219. package/dist/server.d.ts.map +1 -0
  220. package/dist/server.js +44 -0
  221. package/dist/server.js.map +1 -0
  222. package/dist/services/audit/ad-audit.service.d.ts +70 -0
  223. package/dist/services/audit/ad-audit.service.d.ts.map +1 -0
  224. package/dist/services/audit/ad-audit.service.js +1019 -0
  225. package/dist/services/audit/ad-audit.service.js.map +1 -0
  226. package/dist/services/audit/attack-graph.service.d.ts +62 -0
  227. package/dist/services/audit/attack-graph.service.d.ts.map +1 -0
  228. package/dist/services/audit/attack-graph.service.js +702 -0
  229. package/dist/services/audit/attack-graph.service.js.map +1 -0
  230. package/dist/services/audit/audit.service.d.ts +4 -0
  231. package/dist/services/audit/audit.service.d.ts.map +1 -0
  232. package/dist/services/audit/audit.service.js +10 -0
  233. package/dist/services/audit/audit.service.js.map +1 -0
  234. package/dist/services/audit/azure-audit.service.d.ts +37 -0
  235. package/dist/services/audit/azure-audit.service.d.ts.map +1 -0
  236. package/dist/services/audit/azure-audit.service.js +153 -0
  237. package/dist/services/audit/azure-audit.service.js.map +1 -0
  238. package/dist/services/audit/detectors/ad/accounts.detector.d.ts +37 -0
  239. package/dist/services/audit/detectors/ad/accounts.detector.d.ts.map +1 -0
  240. package/dist/services/audit/detectors/ad/accounts.detector.js +881 -0
  241. package/dist/services/audit/detectors/ad/accounts.detector.js.map +1 -0
  242. package/dist/services/audit/detectors/ad/adcs.detector.d.ts +21 -0
  243. package/dist/services/audit/detectors/ad/adcs.detector.d.ts.map +1 -0
  244. package/dist/services/audit/detectors/ad/adcs.detector.js +227 -0
  245. package/dist/services/audit/detectors/ad/adcs.detector.js.map +1 -0
  246. package/dist/services/audit/detectors/ad/advanced.detector.d.ts +63 -0
  247. package/dist/services/audit/detectors/ad/advanced.detector.d.ts.map +1 -0
  248. package/dist/services/audit/detectors/ad/advanced.detector.js +867 -0
  249. package/dist/services/audit/detectors/ad/advanced.detector.js.map +1 -0
  250. package/dist/services/audit/detectors/ad/attack-paths.detector.d.ts +16 -0
  251. package/dist/services/audit/detectors/ad/attack-paths.detector.d.ts.map +1 -0
  252. package/dist/services/audit/detectors/ad/attack-paths.detector.js +369 -0
  253. package/dist/services/audit/detectors/ad/attack-paths.detector.js.map +1 -0
  254. package/dist/services/audit/detectors/ad/compliance.detector.d.ts +28 -0
  255. package/dist/services/audit/detectors/ad/compliance.detector.d.ts.map +1 -0
  256. package/dist/services/audit/detectors/ad/compliance.detector.js +896 -0
  257. package/dist/services/audit/detectors/ad/compliance.detector.js.map +1 -0
  258. package/dist/services/audit/detectors/ad/computers.detector.d.ts +30 -0
  259. package/dist/services/audit/detectors/ad/computers.detector.d.ts.map +1 -0
  260. package/dist/services/audit/detectors/ad/computers.detector.js +799 -0
  261. package/dist/services/audit/detectors/ad/computers.detector.js.map +1 -0
  262. package/dist/services/audit/detectors/ad/gpo.detector.d.ts +17 -0
  263. package/dist/services/audit/detectors/ad/gpo.detector.d.ts.map +1 -0
  264. package/dist/services/audit/detectors/ad/gpo.detector.js +257 -0
  265. package/dist/services/audit/detectors/ad/gpo.detector.js.map +1 -0
  266. package/dist/services/audit/detectors/ad/groups.detector.d.ts +19 -0
  267. package/dist/services/audit/detectors/ad/groups.detector.d.ts.map +1 -0
  268. package/dist/services/audit/detectors/ad/groups.detector.js +488 -0
  269. package/dist/services/audit/detectors/ad/groups.detector.js.map +1 -0
  270. package/dist/services/audit/detectors/ad/index.d.ts +15 -0
  271. package/dist/services/audit/detectors/ad/index.d.ts.map +1 -0
  272. package/dist/services/audit/detectors/ad/index.js +51 -0
  273. package/dist/services/audit/detectors/ad/index.js.map +1 -0
  274. package/dist/services/audit/detectors/ad/kerberos.detector.d.ts +17 -0
  275. package/dist/services/audit/detectors/ad/kerberos.detector.d.ts.map +1 -0
  276. package/dist/services/audit/detectors/ad/kerberos.detector.js +293 -0
  277. package/dist/services/audit/detectors/ad/kerberos.detector.js.map +1 -0
  278. package/dist/services/audit/detectors/ad/monitoring.detector.d.ts +23 -0
  279. package/dist/services/audit/detectors/ad/monitoring.detector.d.ts.map +1 -0
  280. package/dist/services/audit/detectors/ad/monitoring.detector.js +328 -0
  281. package/dist/services/audit/detectors/ad/monitoring.detector.js.map +1 -0
  282. package/dist/services/audit/detectors/ad/network.detector.d.ts +39 -0
  283. package/dist/services/audit/detectors/ad/network.detector.d.ts.map +1 -0
  284. package/dist/services/audit/detectors/ad/network.detector.js +257 -0
  285. package/dist/services/audit/detectors/ad/network.detector.js.map +1 -0
  286. package/dist/services/audit/detectors/ad/password.detector.d.ts +14 -0
  287. package/dist/services/audit/detectors/ad/password.detector.d.ts.map +1 -0
  288. package/dist/services/audit/detectors/ad/password.detector.js +235 -0
  289. package/dist/services/audit/detectors/ad/password.detector.js.map +1 -0
  290. package/dist/services/audit/detectors/ad/permissions.detector.d.ts +20 -0
  291. package/dist/services/audit/detectors/ad/permissions.detector.d.ts.map +1 -0
  292. package/dist/services/audit/detectors/ad/permissions.detector.js +392 -0
  293. package/dist/services/audit/detectors/ad/permissions.detector.js.map +1 -0
  294. package/dist/services/audit/detectors/ad/trusts.detector.d.ts +11 -0
  295. package/dist/services/audit/detectors/ad/trusts.detector.d.ts.map +1 -0
  296. package/dist/services/audit/detectors/ad/trusts.detector.js +186 -0
  297. package/dist/services/audit/detectors/ad/trusts.detector.js.map +1 -0
  298. package/dist/services/audit/detectors/azure/app-security.detector.d.ts +11 -0
  299. package/dist/services/audit/detectors/azure/app-security.detector.d.ts.map +1 -0
  300. package/dist/services/audit/detectors/azure/app-security.detector.js +184 -0
  301. package/dist/services/audit/detectors/azure/app-security.detector.js.map +1 -0
  302. package/dist/services/audit/detectors/azure/conditional-access.detector.d.ts +10 -0
  303. package/dist/services/audit/detectors/azure/conditional-access.detector.d.ts.map +1 -0
  304. package/dist/services/audit/detectors/azure/conditional-access.detector.js +130 -0
  305. package/dist/services/audit/detectors/azure/conditional-access.detector.js.map +1 -0
  306. package/dist/services/audit/detectors/azure/privilege-security.detector.d.ts +8 -0
  307. package/dist/services/audit/detectors/azure/privilege-security.detector.d.ts.map +1 -0
  308. package/dist/services/audit/detectors/azure/privilege-security.detector.js +113 -0
  309. package/dist/services/audit/detectors/azure/privilege-security.detector.js.map +1 -0
  310. package/dist/services/audit/detectors/azure/user-security.detector.d.ts +14 -0
  311. package/dist/services/audit/detectors/azure/user-security.detector.d.ts.map +1 -0
  312. package/dist/services/audit/detectors/azure/user-security.detector.js +198 -0
  313. package/dist/services/audit/detectors/azure/user-security.detector.js.map +1 -0
  314. package/dist/services/audit/detectors/index.d.ts +2 -0
  315. package/dist/services/audit/detectors/index.d.ts.map +1 -0
  316. package/dist/services/audit/detectors/index.js +38 -0
  317. package/dist/services/audit/detectors/index.js.map +1 -0
  318. package/dist/services/audit/response-formatter.d.ts +176 -0
  319. package/dist/services/audit/response-formatter.d.ts.map +1 -0
  320. package/dist/services/audit/response-formatter.js +240 -0
  321. package/dist/services/audit/response-formatter.js.map +1 -0
  322. package/dist/services/audit/scoring.service.d.ts +15 -0
  323. package/dist/services/audit/scoring.service.d.ts.map +1 -0
  324. package/dist/services/audit/scoring.service.js +139 -0
  325. package/dist/services/audit/scoring.service.js.map +1 -0
  326. package/dist/services/auth/crypto.service.d.ts +19 -0
  327. package/dist/services/auth/crypto.service.d.ts.map +1 -0
  328. package/dist/services/auth/crypto.service.js +135 -0
  329. package/dist/services/auth/crypto.service.js.map +1 -0
  330. package/dist/services/auth/errors.d.ts +19 -0
  331. package/dist/services/auth/errors.d.ts.map +1 -0
  332. package/dist/services/auth/errors.js +46 -0
  333. package/dist/services/auth/errors.js.map +1 -0
  334. package/dist/services/auth/token.service.d.ts +41 -0
  335. package/dist/services/auth/token.service.d.ts.map +1 -0
  336. package/dist/services/auth/token.service.js +208 -0
  337. package/dist/services/auth/token.service.js.map +1 -0
  338. package/dist/services/config/config.service.d.ts +6 -0
  339. package/dist/services/config/config.service.d.ts.map +1 -0
  340. package/dist/services/config/config.service.js +64 -0
  341. package/dist/services/config/config.service.js.map +1 -0
  342. package/dist/services/export/export.service.d.ts +28 -0
  343. package/dist/services/export/export.service.d.ts.map +1 -0
  344. package/dist/services/export/export.service.js +28 -0
  345. package/dist/services/export/export.service.js.map +1 -0
  346. package/dist/services/export/formatters/csv.formatter.d.ts +8 -0
  347. package/dist/services/export/formatters/csv.formatter.d.ts.map +1 -0
  348. package/dist/services/export/formatters/csv.formatter.js +46 -0
  349. package/dist/services/export/formatters/csv.formatter.js.map +1 -0
  350. package/dist/services/export/formatters/json.formatter.d.ts +40 -0
  351. package/dist/services/export/formatters/json.formatter.d.ts.map +1 -0
  352. package/dist/services/export/formatters/json.formatter.js +58 -0
  353. package/dist/services/export/formatters/json.formatter.js.map +1 -0
  354. package/dist/services/jobs/azure-job-runner.d.ts +38 -0
  355. package/dist/services/jobs/azure-job-runner.d.ts.map +1 -0
  356. package/dist/services/jobs/azure-job-runner.js +199 -0
  357. package/dist/services/jobs/azure-job-runner.js.map +1 -0
  358. package/dist/services/jobs/index.d.ts +4 -0
  359. package/dist/services/jobs/index.d.ts.map +1 -0
  360. package/dist/services/jobs/index.js +20 -0
  361. package/dist/services/jobs/index.js.map +1 -0
  362. package/dist/services/jobs/job-runner.d.ts +64 -0
  363. package/dist/services/jobs/job-runner.d.ts.map +1 -0
  364. package/dist/services/jobs/job-runner.js +952 -0
  365. package/dist/services/jobs/job-runner.js.map +1 -0
  366. package/dist/services/jobs/job-store.d.ts +27 -0
  367. package/dist/services/jobs/job-store.d.ts.map +1 -0
  368. package/dist/services/jobs/job-store.js +261 -0
  369. package/dist/services/jobs/job-store.js.map +1 -0
  370. package/dist/services/jobs/job.types.d.ts +67 -0
  371. package/dist/services/jobs/job.types.d.ts.map +1 -0
  372. package/dist/services/jobs/job.types.js +36 -0
  373. package/dist/services/jobs/job.types.js.map +1 -0
  374. package/dist/types/ad.types.d.ts +74 -0
  375. package/dist/types/ad.types.d.ts.map +1 -0
  376. package/dist/types/ad.types.js +3 -0
  377. package/dist/types/ad.types.js.map +1 -0
  378. package/dist/types/adcs.types.d.ts +58 -0
  379. package/dist/types/adcs.types.d.ts.map +1 -0
  380. package/dist/types/adcs.types.js +38 -0
  381. package/dist/types/adcs.types.js.map +1 -0
  382. package/dist/types/attack-graph.types.d.ts +135 -0
  383. package/dist/types/attack-graph.types.d.ts.map +1 -0
  384. package/dist/types/attack-graph.types.js +58 -0
  385. package/dist/types/attack-graph.types.js.map +1 -0
  386. package/dist/types/audit.types.d.ts +34 -0
  387. package/dist/types/audit.types.d.ts.map +1 -0
  388. package/dist/types/audit.types.js +3 -0
  389. package/dist/types/audit.types.js.map +1 -0
  390. package/dist/types/azure.types.d.ts +61 -0
  391. package/dist/types/azure.types.d.ts.map +1 -0
  392. package/dist/types/azure.types.js +3 -0
  393. package/dist/types/azure.types.js.map +1 -0
  394. package/dist/types/config.types.d.ts +63 -0
  395. package/dist/types/config.types.d.ts.map +1 -0
  396. package/dist/types/config.types.js +3 -0
  397. package/dist/types/config.types.js.map +1 -0
  398. package/dist/types/error.types.d.ts +33 -0
  399. package/dist/types/error.types.d.ts.map +1 -0
  400. package/dist/types/error.types.js +70 -0
  401. package/dist/types/error.types.js.map +1 -0
  402. package/dist/types/finding.types.d.ts +133 -0
  403. package/dist/types/finding.types.d.ts.map +1 -0
  404. package/dist/types/finding.types.js +3 -0
  405. package/dist/types/finding.types.js.map +1 -0
  406. package/dist/types/gpo.types.d.ts +39 -0
  407. package/dist/types/gpo.types.d.ts.map +1 -0
  408. package/dist/types/gpo.types.js +15 -0
  409. package/dist/types/gpo.types.js.map +1 -0
  410. package/dist/types/token.types.d.ts +26 -0
  411. package/dist/types/token.types.d.ts.map +1 -0
  412. package/dist/types/token.types.js +3 -0
  413. package/dist/types/token.types.js.map +1 -0
  414. package/dist/types/trust.types.d.ts +45 -0
  415. package/dist/types/trust.types.d.ts.map +1 -0
  416. package/dist/types/trust.types.js +71 -0
  417. package/dist/types/trust.types.js.map +1 -0
  418. package/dist/utils/entity-converter.d.ts +17 -0
  419. package/dist/utils/entity-converter.d.ts.map +1 -0
  420. package/dist/utils/entity-converter.js +285 -0
  421. package/dist/utils/entity-converter.js.map +1 -0
  422. package/dist/utils/graph.util.d.ts +66 -0
  423. package/dist/utils/graph.util.d.ts.map +1 -0
  424. package/dist/utils/graph.util.js +382 -0
  425. package/dist/utils/graph.util.js.map +1 -0
  426. package/dist/utils/logger.d.ts +7 -0
  427. package/dist/utils/logger.d.ts.map +1 -0
  428. package/dist/utils/logger.js +86 -0
  429. package/dist/utils/logger.js.map +1 -0
  430. package/dist/utils/type-name-normalizer.d.ts +5 -0
  431. package/dist/utils/type-name-normalizer.d.ts.map +1 -0
  432. package/dist/utils/type-name-normalizer.js +218 -0
  433. package/dist/utils/type-name-normalizer.js.map +1 -0
  434. package/docker-compose.yml +26 -0
  435. package/docs/api/README.md +178 -0
  436. package/docs/api/openapi.yaml +1524 -0
  437. package/eslint.config.js +54 -0
  438. package/jest.config.js +38 -0
  439. package/package.json +97 -0
  440. package/scripts/fetch-ad-cert.sh +142 -0
  441. package/src/.gitkeep +0 -0
  442. package/src/api/.gitkeep +0 -0
  443. package/src/api/controllers/.gitkeep +0 -0
  444. package/src/api/controllers/audit.controller.ts +313 -0
  445. package/src/api/controllers/auth.controller.ts +258 -0
  446. package/src/api/controllers/export.controller.ts +153 -0
  447. package/src/api/controllers/health.controller.ts +16 -0
  448. package/src/api/controllers/jobs.controller.ts +187 -0
  449. package/src/api/controllers/providers.controller.ts +165 -0
  450. package/src/api/dto/.gitkeep +0 -0
  451. package/src/api/dto/AuditRequest.dto.ts +8 -0
  452. package/src/api/dto/AuditResponse.dto.ts +19 -0
  453. package/src/api/dto/TokenRequest.dto.ts +8 -0
  454. package/src/api/dto/TokenResponse.dto.ts +14 -0
  455. package/src/api/middlewares/.gitkeep +0 -0
  456. package/src/api/middlewares/authenticate.ts +203 -0
  457. package/src/api/middlewares/errorHandler.ts +54 -0
  458. package/src/api/middlewares/rateLimit.ts +35 -0
  459. package/src/api/middlewares/validate.ts +32 -0
  460. package/src/api/routes/.gitkeep +0 -0
  461. package/src/api/routes/audit.routes.ts +77 -0
  462. package/src/api/routes/auth.routes.ts +71 -0
  463. package/src/api/routes/export.routes.ts +34 -0
  464. package/src/api/routes/health.routes.ts +14 -0
  465. package/src/api/routes/index.ts +40 -0
  466. package/src/api/routes/providers.routes.ts +24 -0
  467. package/src/api/validators/.gitkeep +0 -0
  468. package/src/api/validators/audit.schemas.ts +59 -0
  469. package/src/api/validators/auth.schemas.ts +59 -0
  470. package/src/app.ts +87 -0
  471. package/src/config/.gitkeep +0 -0
  472. package/src/config/config.schema.ts +108 -0
  473. package/src/config/index.ts +82 -0
  474. package/src/container.ts +221 -0
  475. package/src/data/.gitkeep +0 -0
  476. package/src/data/database.ts +78 -0
  477. package/src/data/jobs/token-cleanup.job.ts +166 -0
  478. package/src/data/migrations/.gitkeep +0 -0
  479. package/src/data/migrations/001_initial_schema.sql +47 -0
  480. package/src/data/migrations/migration.runner.ts +125 -0
  481. package/src/data/models/.gitkeep +0 -0
  482. package/src/data/models/Token.model.ts +35 -0
  483. package/src/data/repositories/.gitkeep +0 -0
  484. package/src/data/repositories/token.repository.ts +160 -0
  485. package/src/providers/.gitkeep +0 -0
  486. package/src/providers/azure/.gitkeep +0 -0
  487. package/src/providers/azure/auth.provider.ts +14 -0
  488. package/src/providers/azure/azure-errors.ts +189 -0
  489. package/src/providers/azure/azure-retry.ts +168 -0
  490. package/src/providers/azure/graph-client.ts +315 -0
  491. package/src/providers/azure/graph.provider.ts +294 -0
  492. package/src/providers/azure/queries/app.queries.ts +9 -0
  493. package/src/providers/azure/queries/policy.queries.ts +9 -0
  494. package/src/providers/azure/queries/user.queries.ts +10 -0
  495. package/src/providers/interfaces/.gitkeep +0 -0
  496. package/src/providers/interfaces/IGraphProvider.ts +117 -0
  497. package/src/providers/interfaces/ILDAPProvider.ts +142 -0
  498. package/src/providers/ldap/.gitkeep +0 -0
  499. package/src/providers/ldap/acl-parser.ts +231 -0
  500. package/src/providers/ldap/ad-mappers.ts +280 -0
  501. package/src/providers/ldap/ldap-client.ts +259 -0
  502. package/src/providers/ldap/ldap-errors.ts +188 -0
  503. package/src/providers/ldap/ldap-retry.ts +267 -0
  504. package/src/providers/ldap/ldap-sanitizer.ts +273 -0
  505. package/src/providers/ldap/ldap.provider.ts +293 -0
  506. package/src/providers/ldap/queries/computer.queries.ts +9 -0
  507. package/src/providers/ldap/queries/group.queries.ts +9 -0
  508. package/src/providers/ldap/queries/user.queries.ts +10 -0
  509. package/src/providers/smb/smb.provider.ts +653 -0
  510. package/src/server.ts +60 -0
  511. package/src/services/.gitkeep +0 -0
  512. package/src/services/audit/.gitkeep +0 -0
  513. package/src/services/audit/ad-audit.service.ts +1481 -0
  514. package/src/services/audit/attack-graph.service.ts +1104 -0
  515. package/src/services/audit/audit.service.ts +12 -0
  516. package/src/services/audit/azure-audit.service.ts +286 -0
  517. package/src/services/audit/detectors/ad/accounts.detector.ts +1232 -0
  518. package/src/services/audit/detectors/ad/adcs.detector.ts +449 -0
  519. package/src/services/audit/detectors/ad/advanced.detector.ts +1270 -0
  520. package/src/services/audit/detectors/ad/attack-paths.detector.ts +600 -0
  521. package/src/services/audit/detectors/ad/compliance.detector.ts +1421 -0
  522. package/src/services/audit/detectors/ad/computers.detector.ts +1188 -0
  523. package/src/services/audit/detectors/ad/gpo.detector.ts +485 -0
  524. package/src/services/audit/detectors/ad/groups.detector.ts +685 -0
  525. package/src/services/audit/detectors/ad/index.ts +84 -0
  526. package/src/services/audit/detectors/ad/kerberos.detector.ts +424 -0
  527. package/src/services/audit/detectors/ad/monitoring.detector.ts +501 -0
  528. package/src/services/audit/detectors/ad/network.detector.ts +538 -0
  529. package/src/services/audit/detectors/ad/password.detector.ts +324 -0
  530. package/src/services/audit/detectors/ad/permissions.detector.ts +637 -0
  531. package/src/services/audit/detectors/ad/trusts.detector.ts +315 -0
  532. package/src/services/audit/detectors/azure/app-security.detector.ts +246 -0
  533. package/src/services/audit/detectors/azure/conditional-access.detector.ts +186 -0
  534. package/src/services/audit/detectors/azure/privilege-security.detector.ts +176 -0
  535. package/src/services/audit/detectors/azure/user-security.detector.ts +280 -0
  536. package/src/services/audit/detectors/index.ts +18 -0
  537. package/src/services/audit/response-formatter.ts +604 -0
  538. package/src/services/audit/scoring.service.ts +234 -0
  539. package/src/services/auth/.gitkeep +0 -0
  540. package/src/services/auth/crypto.service.ts +230 -0
  541. package/src/services/auth/errors.ts +47 -0
  542. package/src/services/auth/token.service.ts +420 -0
  543. package/src/services/config/.gitkeep +0 -0
  544. package/src/services/config/config.service.ts +75 -0
  545. package/src/services/export/.gitkeep +0 -0
  546. package/src/services/export/export.service.ts +99 -0
  547. package/src/services/export/formatters/csv.formatter.ts +124 -0
  548. package/src/services/export/formatters/json.formatter.ts +160 -0
  549. package/src/services/jobs/azure-job-runner.ts +312 -0
  550. package/src/services/jobs/index.ts +9 -0
  551. package/src/services/jobs/job-runner.ts +1280 -0
  552. package/src/services/jobs/job-store.ts +384 -0
  553. package/src/services/jobs/job.types.ts +182 -0
  554. package/src/types/.gitkeep +0 -0
  555. package/src/types/ad.types.ts +91 -0
  556. package/src/types/adcs.types.ts +107 -0
  557. package/src/types/attack-graph.types.ts +260 -0
  558. package/src/types/audit.types.ts +42 -0
  559. package/src/types/azure.types.ts +68 -0
  560. package/src/types/config.types.ts +79 -0
  561. package/src/types/error.types.ts +69 -0
  562. package/src/types/finding.types.ts +284 -0
  563. package/src/types/gpo.types.ts +72 -0
  564. package/src/types/smb2.d.ts +73 -0
  565. package/src/types/token.types.ts +32 -0
  566. package/src/types/trust.types.ts +140 -0
  567. package/src/utils/.gitkeep +0 -0
  568. package/src/utils/entity-converter.ts +453 -0
  569. package/src/utils/graph.util.ts +609 -0
  570. package/src/utils/logger.ts +111 -0
  571. package/src/utils/type-name-normalizer.ts +302 -0
  572. package/tests/.gitkeep +0 -0
  573. package/tests/e2e/.gitkeep +0 -0
  574. package/tests/fixtures/.gitkeep +0 -0
  575. package/tests/integration/.gitkeep +0 -0
  576. package/tests/integration/README.md +156 -0
  577. package/tests/integration/ad-audit.integration.test.ts +216 -0
  578. package/tests/integration/api/.gitkeep +0 -0
  579. package/tests/integration/api/endpoints.integration.test.ts +431 -0
  580. package/tests/integration/auth/jwt-authentication.integration.test.ts +358 -0
  581. package/tests/integration/providers/.gitkeep +0 -0
  582. package/tests/integration/providers/azure-basic.integration.test.ts +167 -0
  583. package/tests/integration/providers/ldap-basic.integration.test.ts +152 -0
  584. package/tests/integration/providers/ldap-connectivity.test.ts +44 -0
  585. package/tests/integration/providers/ldap-provider.integration.test.ts +347 -0
  586. package/tests/mocks/.gitkeep +0 -0
  587. package/tests/setup.ts +16 -0
  588. package/tests/unit/.gitkeep +0 -0
  589. package/tests/unit/api/middlewares/authenticate.test.ts +446 -0
  590. package/tests/unit/providers/.gitkeep +0 -0
  591. package/tests/unit/providers/azure/azure-errors.test.ts +193 -0
  592. package/tests/unit/providers/azure/azure-retry.test.ts +254 -0
  593. package/tests/unit/providers/azure/graph-provider.test.ts +313 -0
  594. package/tests/unit/providers/ldap/ad-mappers.test.ts +392 -0
  595. package/tests/unit/providers/ldap/ldap-provider.test.ts +376 -0
  596. package/tests/unit/providers/ldap/ldap-retry.test.ts +377 -0
  597. package/tests/unit/providers/ldap/ldap-sanitizer.test.ts +301 -0
  598. package/tests/unit/sample.test.ts +19 -0
  599. package/tests/unit/services/.gitkeep +0 -0
  600. package/tests/unit/services/audit/detectors/ad/accounts.detector.test.ts +393 -0
  601. package/tests/unit/services/audit/detectors/ad/advanced.detector.test.ts +380 -0
  602. package/tests/unit/services/audit/detectors/ad/computers.detector.test.ts +440 -0
  603. package/tests/unit/services/audit/detectors/ad/groups.detector.test.ts +276 -0
  604. package/tests/unit/services/audit/detectors/ad/kerberos.detector.test.ts +215 -0
  605. package/tests/unit/services/audit/detectors/ad/password.detector.test.ts +226 -0
  606. package/tests/unit/services/audit/detectors/ad/permissions.detector.test.ts +244 -0
  607. package/tests/unit/services/audit/detectors/azure/app-security.detector.test.ts +349 -0
  608. package/tests/unit/services/audit/detectors/azure/conditional-access.detector.test.ts +374 -0
  609. package/tests/unit/services/audit/detectors/azure/privilege-security.detector.test.ts +374 -0
  610. package/tests/unit/services/audit/detectors/azure/user-security.detector.test.ts +297 -0
  611. package/tests/unit/services/auth/crypto.service.test.ts +296 -0
  612. package/tests/unit/services/auth/token.service.test.ts +579 -0
  613. package/tests/unit/services/export/export.service.test.ts +241 -0
  614. package/tests/unit/services/export/formatters/csv.formatter.test.ts +270 -0
  615. package/tests/unit/services/export/formatters/json.formatter.test.ts +258 -0
  616. package/tests/unit/utils/.gitkeep +0 -0
  617. package/tsconfig.json +50 -0
@@ -0,0 +1,1232 @@
1
+ /**
2
+ * Accounts Security Vulnerability Detector
3
+ *
4
+ * Detects account-related vulnerabilities in AD.
5
+ * Story 1.7: AD Vulnerability Detection Engine
6
+ *
7
+ * Vulnerabilities detected (31):
8
+ *
9
+ * PRIVILEGED ACCOUNTS:
10
+ * - SENSITIVE_DELEGATION (Critical)
11
+ * - DISABLED_ACCOUNT_IN_ADMIN_GROUP (High)
12
+ * - EXPIRED_ACCOUNT_IN_ADMIN_GROUP (High)
13
+ * - SID_HISTORY (High)
14
+ * - NOT_IN_PROTECTED_USERS (High)
15
+ * - DOMAIN_ADMIN_IN_DESCRIPTION (High)
16
+ * - BACKUP_OPERATORS_MEMBER (High)
17
+ * - ACCOUNT_OPERATORS_MEMBER (High)
18
+ * - SERVER_OPERATORS_MEMBER (High)
19
+ * - PRINT_OPERATORS_MEMBER (High)
20
+ *
21
+ * STATUS:
22
+ * - INACTIVE_365_DAYS (Medium)
23
+ * - NEVER_LOGGED_ON (Medium) - Enabled accounts that have never logged in
24
+ * - ACCOUNT_EXPIRE_SOON (Medium) - Accounts expiring within 30 days
25
+ * - ADMIN_LOGON_COUNT_LOW (Low) - Admin accounts with very few logons
26
+ *
27
+ * DANGEROUS PATTERNS:
28
+ * - TEST_ACCOUNT (Medium)
29
+ * - SHARED_ACCOUNT (Medium)
30
+ * - SMARTCARD_NOT_REQUIRED (Medium)
31
+ * - PRIMARYGROUPID_SPOOFING (Medium)
32
+ *
33
+ * SERVICE ACCOUNTS:
34
+ * - SERVICE_ACCOUNT_WITH_SPN (Medium) - Kerberoasting targets
35
+ * - SERVICE_ACCOUNT_NAMING (Low) - Accounts matching service naming patterns
36
+ * - SERVICE_ACCOUNT_OLD_PASSWORD (High) - Passwords > 1 year old
37
+ * - SERVICE_ACCOUNT_PRIVILEGED (Critical) - Service accounts in admin groups
38
+ * - SERVICE_ACCOUNT_NO_PREAUTH (High) - AS-REP Roasting targets
39
+ * - SERVICE_ACCOUNT_WEAK_ENCRYPTION (Medium) - DES/RC4 only encryption
40
+ */
41
+
42
+ import { ADUser } from '../../../../types/ad.types';
43
+ import { Finding } from '../../../../types/finding.types';
44
+ import { toAffectedUserEntities, ldapAttrToString } from '../../../../utils/entity-converter';
45
+
46
+ /**
47
+ * Check for sensitive accounts with unconstrained delegation
48
+ */
49
+ export function detectSensitiveDelegation(users: ADUser[], includeDetails: boolean): Finding {
50
+ const privilegedGroups = [
51
+ 'Domain Admins',
52
+ 'Enterprise Admins',
53
+ 'Schema Admins',
54
+ 'Administrators',
55
+ ];
56
+
57
+ const affected = users.filter((u) => {
58
+ if (!u.userAccountControl || !u.memberOf) return false;
59
+ const hasUnconstrainedDeleg = (u.userAccountControl & 0x80000) !== 0;
60
+ const isPrivileged = u.memberOf.some((dn) =>
61
+ privilegedGroups.some((group) => dn.includes(`CN=${group}`))
62
+ );
63
+ return hasUnconstrainedDeleg && isPrivileged;
64
+ });
65
+
66
+ return {
67
+ type: 'SENSITIVE_DELEGATION',
68
+ severity: 'critical',
69
+ category: 'accounts',
70
+ title: 'Sensitive Account with Delegation',
71
+ description: 'Privileged accounts (Domain/Enterprise Admins) with unconstrained delegation. Extreme security risk.',
72
+ count: affected.length,
73
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Check for disabled accounts still in admin groups
79
+ */
80
+ export function detectDisabledAccountInAdminGroup(users: ADUser[], includeDetails: boolean): Finding {
81
+ const adminGroups = ['Domain Admins', 'Enterprise Admins', 'Schema Admins'];
82
+
83
+ const affected = users.filter((u) => {
84
+ if (!u.userAccountControl || !u.memberOf) return false;
85
+ const isDisabled = (u.userAccountControl & 0x2) !== 0;
86
+ const isInAdminGroup = u.memberOf.some((dn) =>
87
+ adminGroups.some((group) => dn.includes(`CN=${group}`))
88
+ );
89
+ return isDisabled && isInAdminGroup;
90
+ });
91
+
92
+ return {
93
+ type: 'DISABLED_ACCOUNT_IN_ADMIN_GROUP',
94
+ severity: 'high',
95
+ category: 'accounts',
96
+ title: 'Disabled Account in Admin Group',
97
+ description: 'Disabled user accounts still present in privileged groups. Should be removed immediately.',
98
+ count: affected.length,
99
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Check for expired accounts still in admin groups
105
+ */
106
+ export function detectExpiredAccountInAdminGroup(users: ADUser[], includeDetails: boolean): Finding {
107
+ const adminGroups = ['Domain Admins', 'Enterprise Admins', 'Schema Admins'];
108
+ const now = Date.now();
109
+
110
+ const affected = users.filter((u) => {
111
+ if (!u.memberOf) return false;
112
+ const accountExpires = (u as any)['accountExpires'] as Date | undefined;
113
+ const isExpired = accountExpires && accountExpires.getTime() < now;
114
+ const isInAdminGroup = u.memberOf.some((dn: string) =>
115
+ adminGroups.some((group) => dn.includes(`CN=${group}`))
116
+ );
117
+ return isExpired && isInAdminGroup;
118
+ });
119
+
120
+ return {
121
+ type: 'EXPIRED_ACCOUNT_IN_ADMIN_GROUP',
122
+ severity: 'high',
123
+ category: 'accounts',
124
+ title: 'Expired Account in Admin Group',
125
+ description: 'Expired user accounts still present in privileged groups. Should be removed immediately.',
126
+ count: affected.length,
127
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Check for SID history attribute
133
+ * Note: LDAP attribute name can vary in case (sIDHistory, sidhistory, etc.)
134
+ */
135
+ export function detectSidHistory(users: ADUser[], includeDetails: boolean): Finding {
136
+ const affected = users.filter((u) => {
137
+ // Check multiple possible attribute names (case-insensitive)
138
+ const userObj = u as Record<string, unknown>;
139
+ const sidHistory =
140
+ userObj['sIDHistory'] ??
141
+ userObj['sidhistory'] ??
142
+ userObj['SIDHistory'] ??
143
+ userObj['sidHistory'];
144
+
145
+ // Check if attribute exists and has value
146
+ if (!sidHistory) return false;
147
+
148
+ // Handle array or single value
149
+ if (Array.isArray(sidHistory)) {
150
+ return sidHistory.length > 0;
151
+ }
152
+ return !!sidHistory;
153
+ });
154
+
155
+ return {
156
+ type: 'SID_HISTORY',
157
+ severity: 'high',
158
+ category: 'accounts',
159
+ title: 'SID History Present',
160
+ description: 'User accounts with sIDHistory attribute. Can be abused for privilege escalation.',
161
+ count: affected.length,
162
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Check for privileged accounts not in Protected Users group
168
+ */
169
+ export function detectNotInProtectedUsers(users: ADUser[], includeDetails: boolean): Finding {
170
+ const privilegedGroups = ['Domain Admins', 'Enterprise Admins', 'Schema Admins'];
171
+
172
+ const affected = users.filter((u) => {
173
+ if (!u.memberOf) return false;
174
+ const isPrivileged = u.memberOf.some((dn) =>
175
+ privilegedGroups.some((group) => dn.includes(`CN=${group}`))
176
+ );
177
+ const isInProtectedUsers = u.memberOf.some((dn) => dn.includes('CN=Protected Users'));
178
+ return isPrivileged && !isInProtectedUsers;
179
+ });
180
+
181
+ return {
182
+ type: 'NOT_IN_PROTECTED_USERS',
183
+ severity: 'high',
184
+ category: 'accounts',
185
+ title: 'Not in Protected Users Group',
186
+ description: 'Privileged accounts not in Protected Users group. Missing additional security protections.',
187
+ count: affected.length,
188
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
189
+ };
190
+ }
191
+
192
+ /**
193
+ * Check for domain admin keywords in description
194
+ */
195
+ export function detectDomainAdminInDescription(users: ADUser[], includeDetails: boolean): Finding {
196
+ const sensitiveKeywords = [
197
+ /domain\s*admin/i,
198
+ /enterprise\s*admin/i,
199
+ /administrator/i,
200
+ /admin\s*account/i,
201
+ /privileged/i,
202
+ ];
203
+
204
+ const affected = users.filter((u) => {
205
+ const description = ldapAttrToString((u as any)['description']);
206
+ if (!description) return false;
207
+ return sensitiveKeywords.some((pattern) => pattern.test(description));
208
+ });
209
+
210
+ return {
211
+ type: 'DOMAIN_ADMIN_IN_DESCRIPTION',
212
+ severity: 'high',
213
+ category: 'accounts',
214
+ title: 'Sensitive Terms in Description',
215
+ description: 'User accounts with admin/privileged keywords in description field. Information disclosure.',
216
+ count: affected.length,
217
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
218
+ };
219
+ }
220
+
221
+ /**
222
+ * Check for Backup Operators membership
223
+ */
224
+ export function detectBackupOperatorsMember(users: ADUser[], includeDetails: boolean): Finding {
225
+ const affected = users.filter((u) => {
226
+ if (!u.memberOf) return false;
227
+ return u.memberOf.some((dn) => dn.includes('CN=Backup Operators'));
228
+ });
229
+
230
+ return {
231
+ type: 'BACKUP_OPERATORS_MEMBER',
232
+ severity: 'high',
233
+ category: 'accounts',
234
+ title: 'Backup Operators Member',
235
+ description: 'Users in Backup Operators group. Can backup/restore files and bypass ACLs.',
236
+ count: affected.length,
237
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
238
+ };
239
+ }
240
+
241
+ /**
242
+ * Check for Account Operators membership
243
+ */
244
+ export function detectAccountOperatorsMember(users: ADUser[], includeDetails: boolean): Finding {
245
+ const affected = users.filter((u) => {
246
+ if (!u.memberOf) return false;
247
+ return u.memberOf.some((dn) => dn.includes('CN=Account Operators'));
248
+ });
249
+
250
+ return {
251
+ type: 'ACCOUNT_OPERATORS_MEMBER',
252
+ severity: 'high',
253
+ category: 'accounts',
254
+ title: 'Account Operators Member',
255
+ description: 'Users in Account Operators group. Can create/modify user accounts.',
256
+ count: affected.length,
257
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
258
+ };
259
+ }
260
+
261
+ /**
262
+ * Check for Server Operators membership
263
+ */
264
+ export function detectServerOperatorsMember(users: ADUser[], includeDetails: boolean): Finding {
265
+ const affected = users.filter((u) => {
266
+ if (!u.memberOf) return false;
267
+ return u.memberOf.some((dn) => dn.includes('CN=Server Operators'));
268
+ });
269
+
270
+ return {
271
+ type: 'SERVER_OPERATORS_MEMBER',
272
+ severity: 'high',
273
+ category: 'accounts',
274
+ title: 'Server Operators Member',
275
+ description: 'Users in Server Operators group. Can manage domain controllers.',
276
+ count: affected.length,
277
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
278
+ };
279
+ }
280
+
281
+ /**
282
+ * Check for Print Operators membership
283
+ */
284
+ export function detectPrintOperatorsMember(users: ADUser[], includeDetails: boolean): Finding {
285
+ const affected = users.filter((u) => {
286
+ if (!u.memberOf) return false;
287
+ return u.memberOf.some((dn) => dn.includes('CN=Print Operators'));
288
+ });
289
+
290
+ return {
291
+ type: 'PRINT_OPERATORS_MEMBER',
292
+ severity: 'high',
293
+ category: 'accounts',
294
+ title: 'Print Operators Member',
295
+ description: 'Users in Print Operators group. Can load drivers and manage printers on DCs.',
296
+ count: affected.length,
297
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
298
+ };
299
+ }
300
+
301
+ /**
302
+ * Check for inactive accounts (365+ days)
303
+ */
304
+ export function detectInactive365Days(users: ADUser[], includeDetails: boolean): Finding {
305
+ const now = Date.now();
306
+ const oneYearAgo = now - 365 * 24 * 60 * 60 * 1000;
307
+
308
+ const affected = users.filter((u) => {
309
+ if (!u.lastLogon) return false;
310
+ return u.lastLogon.getTime() < oneYearAgo;
311
+ });
312
+
313
+ return {
314
+ type: 'INACTIVE_365_DAYS',
315
+ severity: 'medium',
316
+ category: 'accounts',
317
+ title: 'Inactive 365+ Days',
318
+ description: 'User accounts inactive for 365+ days. Should be disabled or deleted.',
319
+ count: affected.length,
320
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
321
+ };
322
+ }
323
+
324
+ /**
325
+ * Check for stale accounts (180+ days inactive)
326
+ * PingCastle: StaleAccount
327
+ */
328
+ export function detectStaleAccount(users: ADUser[], includeDetails: boolean): Finding {
329
+ const now = Date.now();
330
+ const sixMonthsAgo = now - 180 * 24 * 60 * 60 * 1000;
331
+
332
+ const affected = users.filter((u) => {
333
+ // Must be enabled
334
+ if (!u.enabled) return false;
335
+ // Check if last logon is older than 180 days
336
+ if (!u.lastLogon) return false;
337
+ const lastLogonTime = u.lastLogon instanceof Date ? u.lastLogon.getTime() : new Date(u.lastLogon).getTime();
338
+ if (isNaN(lastLogonTime)) return false;
339
+ return lastLogonTime < sixMonthsAgo;
340
+ });
341
+
342
+ return {
343
+ type: 'STALE_ACCOUNT',
344
+ severity: 'high',
345
+ category: 'accounts',
346
+ title: 'Stale Account (180+ Days)',
347
+ description: 'Enabled user accounts inactive for 180+ days. Stale accounts increase attack surface and should be reviewed.',
348
+ count: affected.length,
349
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
350
+ };
351
+ }
352
+
353
+ /**
354
+ * Check for enabled accounts that have never logged on
355
+ * Indicates orphaned, unused, or provisioning issues
356
+ */
357
+ export function detectNeverLoggedOn(users: ADUser[], includeDetails: boolean): Finding {
358
+ const affected = users.filter((u) => {
359
+ // Must be enabled
360
+ if (!u.enabled) return false;
361
+ // Never logged on
362
+ return !u.lastLogon;
363
+ });
364
+
365
+ return {
366
+ type: 'NEVER_LOGGED_ON',
367
+ severity: 'medium',
368
+ category: 'accounts',
369
+ title: 'Never Logged On',
370
+ description:
371
+ 'Enabled user accounts that have never logged into the domain. May indicate orphaned accounts, provisioning issues, or unused accounts that should be disabled.',
372
+ count: affected.length,
373
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
374
+ };
375
+ }
376
+
377
+ /**
378
+ * Convert Windows FILETIME to JavaScript Date
379
+ * FILETIME: 100-nanosecond intervals since January 1, 1601
380
+ */
381
+ function filetimeToDate(filetime: string | number | undefined): Date | null {
382
+ if (!filetime) return null;
383
+ const ft = typeof filetime === 'string' ? BigInt(filetime) : BigInt(filetime);
384
+ // 0 or max value (never expires) should return null
385
+ if (ft === BigInt(0) || ft === BigInt('9223372036854775807')) return null;
386
+ // Convert to milliseconds since Unix epoch
387
+ // FILETIME epoch is 1601-01-01, Unix epoch is 1970-01-01
388
+ // Difference: 11644473600000 milliseconds
389
+ const ms = Number(ft / BigInt(10000)) - 11644473600000;
390
+ return new Date(ms);
391
+ }
392
+
393
+ /**
394
+ * Check for accounts expiring within 30 days
395
+ * Useful for proactive account management
396
+ */
397
+ export function detectAccountExpireSoon(users: ADUser[], includeDetails: boolean): Finding {
398
+ const now = Date.now();
399
+ const thirtyDaysFromNow = now + 30 * 24 * 60 * 60 * 1000;
400
+
401
+ const affected = users.filter((u) => {
402
+ // Must be enabled
403
+ if (!u.enabled) return false;
404
+ // Check accountExpires
405
+ const expiresDate = filetimeToDate(u.accountExpires);
406
+ if (!expiresDate) return false; // Never expires
407
+ // Expiring within 30 days but not already expired
408
+ return expiresDate.getTime() > now && expiresDate.getTime() <= thirtyDaysFromNow;
409
+ });
410
+
411
+ return {
412
+ type: 'ACCOUNT_EXPIRE_SOON',
413
+ severity: 'medium',
414
+ category: 'accounts',
415
+ title: 'Account Expiring Soon',
416
+ description:
417
+ 'User accounts set to expire within the next 30 days. Review if these expirations are intentional or if accounts need to be extended.',
418
+ count: affected.length,
419
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
420
+ };
421
+ }
422
+
423
+ /**
424
+ * Check for admin accounts with very low logon count
425
+ * May indicate unused admin accounts or recently created accounts with elevated privileges
426
+ */
427
+ export function detectAdminLogonCountLow(users: ADUser[], includeDetails: boolean): Finding {
428
+ const affected = users.filter((u) => {
429
+ // Must be enabled
430
+ if (!u.enabled) return false;
431
+ // Must be marked as admin (adminCount = 1)
432
+ if (u.adminCount !== 1) return false;
433
+ // Check logon count (accessible via index signature)
434
+ const logonCount = (u as any)['logonCount'] as number | undefined;
435
+ // Low logon count (less than 5)
436
+ return logonCount !== undefined && logonCount < 5;
437
+ });
438
+
439
+ return {
440
+ type: 'ADMIN_LOGON_COUNT_LOW',
441
+ severity: 'low',
442
+ category: 'accounts',
443
+ title: 'Admin Account with Low Logon Count',
444
+ description:
445
+ 'Administrative accounts (adminCount=1) with fewer than 5 logons. May indicate unused privileged accounts that should be reviewed or disabled.',
446
+ count: affected.length,
447
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
448
+ };
449
+ }
450
+
451
+ /**
452
+ * Check for test accounts
453
+ */
454
+ export function detectTestAccount(users: ADUser[], includeDetails: boolean): Finding {
455
+ const testPatterns = [/^test/i, /test$/i, /_test/i, /\.test/i, /^demo/i, /^temp/i];
456
+
457
+ const affected = users.filter((u) => {
458
+ return testPatterns.some((pattern) => pattern.test(u.sAMAccountName));
459
+ });
460
+
461
+ return {
462
+ type: 'TEST_ACCOUNT',
463
+ severity: 'medium',
464
+ category: 'accounts',
465
+ title: 'Test Account',
466
+ description: 'User accounts with test/demo/temp naming. Should be removed from production.',
467
+ count: affected.length,
468
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
469
+ };
470
+ }
471
+
472
+ /**
473
+ * Check for shared accounts
474
+ */
475
+ export function detectSharedAccount(users: ADUser[], includeDetails: boolean): Finding {
476
+ const sharedPatterns = [/^shared/i, /^common/i, /^generic/i, /^service/i, /^svc/i];
477
+
478
+ const affected = users.filter((u) => {
479
+ return sharedPatterns.some((pattern) => pattern.test(u.sAMAccountName));
480
+ });
481
+
482
+ return {
483
+ type: 'SHARED_ACCOUNT',
484
+ severity: 'medium',
485
+ category: 'accounts',
486
+ title: 'Shared Account',
487
+ description: 'User accounts with shared/generic naming. Prevents proper accountability.',
488
+ count: affected.length,
489
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
490
+ };
491
+ }
492
+
493
+ /**
494
+ * Check for accounts without smartcard requirement
495
+ *
496
+ * Detects enabled user accounts that don't have SMARTCARD_REQUIRED flag set.
497
+ * In high-security environments, critical accounts should require smartcard.
498
+ *
499
+ * Note: This is a broad check. For admin-specific detection, use ADMIN_NO_SMARTCARD.
500
+ * UAC flag 0x40000 = SMARTCARD_REQUIRED
501
+ */
502
+ export function detectSmartcardNotRequired(users: ADUser[], includeDetails: boolean): Finding {
503
+ // Only check enabled accounts with adminCount=1 (privileged accounts)
504
+ // Regular users without smartcard is expected in most environments
505
+ const affected = users.filter((u) => {
506
+ if (!u.enabled) return false;
507
+ if (!u.adminCount || u.adminCount !== 1) return false;
508
+
509
+ const uac = u.userAccountControl || 0;
510
+ // Check if SMARTCARD_REQUIRED is NOT set
511
+ return (uac & 0x40000) === 0;
512
+ });
513
+
514
+ return {
515
+ type: 'SMARTCARD_NOT_REQUIRED',
516
+ severity: 'medium',
517
+ category: 'accounts',
518
+ title: 'Smartcard Not Required',
519
+ description:
520
+ 'Privileged accounts (adminCount=1) without smartcard requirement. ' +
521
+ 'High-value accounts should require strong authentication.',
522
+ count: affected.length,
523
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
524
+ };
525
+ }
526
+
527
+ /**
528
+ * Check for primaryGroupID spoofing
529
+ */
530
+ export function detectPrimaryGroupIdSpoofing(users: ADUser[], includeDetails: boolean): Finding {
531
+ const affected = users.filter((u) => {
532
+ const primaryGroupId = (u as any).primaryGroupID;
533
+ if (!primaryGroupId) return false;
534
+ return primaryGroupId !== 513;
535
+ });
536
+
537
+ return {
538
+ type: 'PRIMARYGROUPID_SPOOFING',
539
+ severity: 'medium',
540
+ category: 'accounts',
541
+ title: 'primaryGroupID Spoofing',
542
+ description: 'User accounts with non-standard primaryGroupID. Can be used to hide group membership.',
543
+ count: affected.length,
544
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
545
+ };
546
+ }
547
+
548
+ // ==================== SERVICE ACCOUNT DETECTORS ====================
549
+
550
+ /**
551
+ * Service account naming patterns for detection
552
+ */
553
+ const SERVICE_ACCOUNT_PATTERNS = [
554
+ /^svc[_-]/i, // svc_xxx, svc-xxx
555
+ /[_-]svc$/i, // xxx_svc, xxx-svc
556
+ /^service[_-]/i, // service_xxx, service-xxx
557
+ /[_-]service$/i, // xxx_service
558
+ /^sa[_-]/i, // sa_xxx (service account prefix)
559
+ /[_-]sa$/i, // xxx_sa
560
+ /^app[_-]/i, // app_xxx (application account)
561
+ /^sql[_-]/i, // sql_xxx (SQL service)
562
+ /^iis[_-]/i, // iis_xxx (IIS service)
563
+ /^web[_-]/i, // web_xxx
564
+ /^batch[_-]/i, // batch_xxx
565
+ /^task[_-]/i, // task_xxx
566
+ /^job[_-]/i, // job_xxx
567
+ /^daemon[_-]/i, // daemon_xxx
568
+ /^agent[_-]/i, // agent_xxx
569
+ ];
570
+
571
+ /**
572
+ * Get service principal names from user (handles index signature)
573
+ */
574
+ function getServicePrincipalNames(user: ADUser): string[] {
575
+ const spn = (user as any)['servicePrincipalName'];
576
+ if (!spn) return [];
577
+ if (Array.isArray(spn)) return spn;
578
+ return [spn as string];
579
+ }
580
+
581
+ /**
582
+ * Check if user is a service account (has SPN or matches naming pattern)
583
+ */
584
+ function isServiceAccount(user: ADUser): boolean {
585
+ // Has SPN = definitely a service account
586
+ const spns = getServicePrincipalNames(user);
587
+ if (spns.length > 0) {
588
+ return true;
589
+ }
590
+ // Matches service naming pattern
591
+ return SERVICE_ACCOUNT_PATTERNS.some((pattern) => pattern.test(user.sAMAccountName));
592
+ }
593
+
594
+ /**
595
+ * SERVICE_ACCOUNT_WITH_SPN: User accounts with Service Principal Name
596
+ * Kerberoasting targets - attackers can request service tickets and crack them offline
597
+ */
598
+ export function detectServiceAccountWithSpn(users: ADUser[], includeDetails: boolean): Finding {
599
+ const affected = users.filter((u) => {
600
+ // Must be enabled and have SPN
601
+ const spns = getServicePrincipalNames(u);
602
+ if (spns.length === 0) return false;
603
+ // Exclude disabled accounts
604
+ if (u.userAccountControl && (u.userAccountControl & 0x2) !== 0) return false;
605
+ return true;
606
+ });
607
+
608
+ return {
609
+ type: 'SERVICE_ACCOUNT_WITH_SPN',
610
+ severity: 'medium',
611
+ category: 'accounts',
612
+ title: 'Service Account with SPN (Kerberoasting Target)',
613
+ description:
614
+ 'User accounts with Service Principal Name configured. These accounts are targets for Kerberoasting attacks where attackers request TGS tickets and crack them offline.',
615
+ count: affected.length,
616
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
617
+ details:
618
+ affected.length > 0
619
+ ? {
620
+ recommendation:
621
+ 'Use gMSA (Group Managed Service Accounts) instead. For existing accounts, ensure strong passwords (25+ chars) and regular rotation.',
622
+ spnCount: affected.reduce((sum, u) => sum + getServicePrincipalNames(u).length, 0),
623
+ }
624
+ : undefined,
625
+ };
626
+ }
627
+
628
+ /**
629
+ * SERVICE_ACCOUNT_NAMING: Accounts matching service naming conventions
630
+ */
631
+ export function detectServiceAccountNaming(users: ADUser[], includeDetails: boolean): Finding {
632
+ const affected = users.filter((u) => {
633
+ // Only accounts matching naming patterns but WITHOUT SPN
634
+ // (accounts WITH SPN are covered by SERVICE_ACCOUNT_WITH_SPN)
635
+ const spns = getServicePrincipalNames(u);
636
+ if (spns.length > 0) return false;
637
+ // Exclude disabled accounts
638
+ if (u.userAccountControl && (u.userAccountControl & 0x2) !== 0) return false;
639
+ return SERVICE_ACCOUNT_PATTERNS.some((pattern) => pattern.test(u.sAMAccountName));
640
+ });
641
+
642
+ return {
643
+ type: 'SERVICE_ACCOUNT_NAMING',
644
+ severity: 'low',
645
+ category: 'accounts',
646
+ title: 'Service Account by Naming Convention',
647
+ description:
648
+ 'User accounts matching service account naming patterns (svc_, _svc, service, etc.) without SPN. Review if these are actual service accounts.',
649
+ count: affected.length,
650
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
651
+ };
652
+ }
653
+
654
+ /**
655
+ * SERVICE_ACCOUNT_OLD_PASSWORD: Service accounts with old passwords
656
+ * High risk - service account passwords should be rotated regularly
657
+ */
658
+ export function detectServiceAccountOldPassword(users: ADUser[], includeDetails: boolean): Finding {
659
+ const now = Date.now();
660
+ const oneYearAgo = now - 365 * 24 * 60 * 60 * 1000;
661
+
662
+ const affected = users.filter((u) => {
663
+ // Must be a service account
664
+ if (!isServiceAccount(u)) return false;
665
+ // Must be enabled
666
+ if (u.userAccountControl && (u.userAccountControl & 0x2) !== 0) return false;
667
+ // Password must be older than 1 year
668
+ if (!u.passwordLastSet) return true; // Never set = very old
669
+ return u.passwordLastSet.getTime() < oneYearAgo;
670
+ });
671
+
672
+ return {
673
+ type: 'SERVICE_ACCOUNT_OLD_PASSWORD',
674
+ severity: 'high',
675
+ category: 'accounts',
676
+ title: 'Service Account with Old Password',
677
+ description:
678
+ 'Service accounts with passwords not changed in over 1 year. These accounts are high-value targets and passwords should be rotated regularly.',
679
+ count: affected.length,
680
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
681
+ details:
682
+ affected.length > 0
683
+ ? {
684
+ recommendation:
685
+ 'Rotate service account passwords every 90 days or migrate to gMSA for automatic password management.',
686
+ }
687
+ : undefined,
688
+ };
689
+ }
690
+
691
+ /**
692
+ * SERVICE_ACCOUNT_PRIVILEGED: Service accounts in privileged groups
693
+ * Critical - service accounts should not be domain admins
694
+ */
695
+ export function detectServiceAccountPrivileged(users: ADUser[], includeDetails: boolean): Finding {
696
+ const privilegedGroups = [
697
+ 'Domain Admins',
698
+ 'Enterprise Admins',
699
+ 'Schema Admins',
700
+ 'Administrators',
701
+ 'Backup Operators',
702
+ 'Account Operators',
703
+ 'Server Operators',
704
+ ];
705
+
706
+ const affected = users.filter((u) => {
707
+ // Must be a service account
708
+ if (!isServiceAccount(u)) return false;
709
+ // Must be enabled
710
+ if (u.userAccountControl && (u.userAccountControl & 0x2) !== 0) return false;
711
+ // Check if in privileged groups
712
+ if (!u.memberOf) return false;
713
+ return u.memberOf.some((dn) => privilegedGroups.some((group) => dn.includes(`CN=${group}`)));
714
+ });
715
+
716
+ return {
717
+ type: 'SERVICE_ACCOUNT_PRIVILEGED',
718
+ severity: 'critical',
719
+ category: 'accounts',
720
+ title: 'Service Account in Privileged Group',
721
+ description:
722
+ 'Service accounts with membership in privileged groups (Domain Admins, etc.). If compromised, attackers gain full domain control.',
723
+ count: affected.length,
724
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
725
+ details:
726
+ affected.length > 0
727
+ ? {
728
+ recommendation:
729
+ 'Remove service accounts from privileged groups. Grant only the minimum permissions needed for the service to function.',
730
+ }
731
+ : undefined,
732
+ };
733
+ }
734
+
735
+ /**
736
+ * SERVICE_ACCOUNT_NO_PREAUTH: Service accounts without Kerberos pre-authentication
737
+ * AS-REP Roasting target
738
+ */
739
+ export function detectServiceAccountNoPreauth(users: ADUser[], includeDetails: boolean): Finding {
740
+ const DONT_REQUIRE_PREAUTH = 0x400000;
741
+
742
+ const affected = users.filter((u) => {
743
+ // Must be a service account
744
+ if (!isServiceAccount(u)) return false;
745
+ // Must be enabled
746
+ if (!u.userAccountControl) return false;
747
+ if ((u.userAccountControl & 0x2) !== 0) return false;
748
+ // Check for "Do not require Kerberos preauthentication"
749
+ return (u.userAccountControl & DONT_REQUIRE_PREAUTH) !== 0;
750
+ });
751
+
752
+ return {
753
+ type: 'SERVICE_ACCOUNT_NO_PREAUTH',
754
+ severity: 'high',
755
+ category: 'accounts',
756
+ title: 'Service Account Without Pre-Authentication (AS-REP Roasting)',
757
+ description:
758
+ 'Service accounts with "Do not require Kerberos pre-authentication" enabled. Attackers can request AS-REP tickets and crack them offline.',
759
+ count: affected.length,
760
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
761
+ details:
762
+ affected.length > 0
763
+ ? {
764
+ recommendation: 'Enable Kerberos pre-authentication for all service accounts.',
765
+ }
766
+ : undefined,
767
+ };
768
+ }
769
+
770
+ /**
771
+ * SERVICE_ACCOUNT_WEAK_ENCRYPTION: Service accounts using weak Kerberos encryption
772
+ */
773
+ export function detectServiceAccountWeakEncryption(users: ADUser[], includeDetails: boolean): Finding {
774
+ // msDS-SupportedEncryptionTypes bit flags
775
+ // 0x1 = DES-CBC-CRC, 0x2 = DES-CBC-MD5 (both weak)
776
+ // 0x4 = RC4-HMAC (weak), 0x8 = AES128, 0x10 = AES256
777
+
778
+ const affected = users.filter((u) => {
779
+ // Must be a service account
780
+ if (!isServiceAccount(u)) return false;
781
+ // Must be enabled
782
+ if (u.userAccountControl && (u.userAccountControl & 0x2) !== 0) return false;
783
+
784
+ const encTypes = (u as any)['msDS-SupportedEncryptionTypes'];
785
+ if (!encTypes) return false;
786
+
787
+ const encTypesNum = typeof encTypes === 'string' ? parseInt(encTypes, 10) : encTypes;
788
+ // Check if only weak encryption types are enabled (DES or RC4 only, no AES)
789
+ const hasOnlyWeak = (encTypesNum & 0x7) !== 0 && (encTypesNum & 0x18) === 0;
790
+ return hasOnlyWeak;
791
+ });
792
+
793
+ return {
794
+ type: 'SERVICE_ACCOUNT_WEAK_ENCRYPTION',
795
+ severity: 'medium',
796
+ category: 'accounts',
797
+ title: 'Service Account Using Weak Kerberos Encryption',
798
+ description:
799
+ 'Service accounts configured to use only weak Kerberos encryption (DES/RC4) without AES. Makes offline cracking easier.',
800
+ count: affected.length,
801
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
802
+ details:
803
+ affected.length > 0
804
+ ? {
805
+ recommendation: 'Enable AES128 and AES256 encryption for all service accounts.',
806
+ }
807
+ : undefined,
808
+ };
809
+ }
810
+
811
+ // ==================== PHASE 2C DETECTORS ====================
812
+
813
+ /**
814
+ * Detect orphaned adminCount flag
815
+ * Users with adminCount=1 but not actually in any admin group
816
+ */
817
+ export function detectAdminCountOrphaned(users: ADUser[], includeDetails: boolean): Finding {
818
+ const adminGroups = [
819
+ 'Domain Admins',
820
+ 'Enterprise Admins',
821
+ 'Schema Admins',
822
+ 'Administrators',
823
+ 'Account Operators',
824
+ 'Server Operators',
825
+ 'Backup Operators',
826
+ 'Print Operators',
827
+ ];
828
+
829
+ const affected = users.filter((u) => {
830
+ // Must have adminCount=1
831
+ if (u.adminCount !== 1) return false;
832
+
833
+ // Check if actually in an admin group
834
+ const memberOf = u['memberOf'] as string[] | undefined;
835
+ if (!memberOf || memberOf.length === 0) return true; // adminCount but no group membership
836
+
837
+ const isInAdminGroup = memberOf.some((dn) =>
838
+ adminGroups.some((group) => dn.toLowerCase().includes(`cn=${group.toLowerCase()}`))
839
+ );
840
+
841
+ return !isInAdminGroup; // adminCount=1 but not in admin group
842
+ });
843
+
844
+ return {
845
+ type: 'ADMIN_COUNT_ORPHANED',
846
+ severity: 'medium',
847
+ category: 'accounts',
848
+ title: 'Orphaned AdminCount Flag',
849
+ description:
850
+ 'Accounts with adminCount=1 but not in any privileged group. This may indicate removed admins that still have residual privileges or SDProp protection.',
851
+ count: affected.length,
852
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
853
+ details:
854
+ affected.length > 0
855
+ ? {
856
+ recommendation:
857
+ 'Review these accounts. If no longer admins, clear adminCount flag and reset ACLs to allow proper inheritance.',
858
+ impact: 'Accounts may still have protected ACLs preventing proper management.',
859
+ }
860
+ : undefined,
861
+ };
862
+ }
863
+
864
+ /**
865
+ * Detect privileged accounts with SPNs (kerberoastable)
866
+ * Admin accounts should NOT have SPNs as they become kerberoasting targets
867
+ */
868
+ export function detectPrivilegedAccountSpn(users: ADUser[], includeDetails: boolean): Finding {
869
+ const affected = users.filter((u) => {
870
+ // Must be privileged (adminCount=1)
871
+ if (u.adminCount !== 1) return false;
872
+ // Must be enabled
873
+ if (!u.enabled) return false;
874
+
875
+ // Must have SPN
876
+ const spn = u['servicePrincipalName'];
877
+ const hasSPN = spn && Array.isArray(spn) && spn.length > 0;
878
+
879
+ return hasSPN;
880
+ });
881
+
882
+ return {
883
+ type: 'PRIVILEGED_ACCOUNT_SPN',
884
+ severity: 'high',
885
+ category: 'accounts',
886
+ title: 'Privileged Account with SPN',
887
+ description:
888
+ 'Privileged accounts (adminCount=1) have Service Principal Names configured. These accounts are vulnerable to Kerberoasting attacks.',
889
+ count: affected.length,
890
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
891
+ details:
892
+ affected.length > 0
893
+ ? {
894
+ attackVector: 'Request TGS ticket → Offline crack password → Full admin access',
895
+ recommendation:
896
+ 'Remove SPNs from admin accounts. Use dedicated service accounts (preferably gMSA) for services.',
897
+ criticalRisk: 'Compromising these accounts grants immediate Domain Admin or equivalent access.',
898
+ }
899
+ : undefined,
900
+ };
901
+ }
902
+
903
+ /**
904
+ * Detect admin accounts without smartcard requirement
905
+ * Privileged accounts should require smartcard authentication
906
+ */
907
+ export function detectAdminNoSmartcard(users: ADUser[], includeDetails: boolean): Finding {
908
+ const affected = users.filter((u) => {
909
+ // Must be privileged (adminCount=1)
910
+ if (u.adminCount !== 1) return false;
911
+ // Must be enabled
912
+ if (!u.enabled) return false;
913
+
914
+ // Check SMARTCARD_REQUIRED flag (0x40000)
915
+ const smartcardRequired = u.userAccountControl ? (u.userAccountControl & 0x40000) !== 0 : false;
916
+
917
+ return !smartcardRequired;
918
+ });
919
+
920
+ return {
921
+ type: 'ADMIN_NO_SMARTCARD',
922
+ severity: 'medium',
923
+ category: 'accounts',
924
+ title: 'Admin Account Without Smartcard Requirement',
925
+ description:
926
+ 'Privileged accounts can authenticate with passwords instead of smartcards. Passwords are more vulnerable to theft and phishing.',
927
+ count: affected.length,
928
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
929
+ details:
930
+ affected.length > 0
931
+ ? {
932
+ recommendation:
933
+ 'Enable "Smart card is required for interactive logon" for all admin accounts.',
934
+ benefits: [
935
+ 'Eliminates password-based attacks (phishing, credential theft)',
936
+ 'Provides two-factor authentication',
937
+ 'Reduces risk of credential replay attacks',
938
+ ],
939
+ }
940
+ : undefined,
941
+ };
942
+ }
943
+
944
+ /**
945
+ * Detect service accounts with interactive logon capability
946
+ * Service accounts should be denied interactive logon
947
+ */
948
+ export function detectServiceAccountInteractive(users: ADUser[], includeDetails: boolean): Finding {
949
+ const affected = users.filter((u) => {
950
+ // Must be a service account (has SPN or matches naming pattern)
951
+ const spn = u['servicePrincipalName'];
952
+ const hasSPN = spn && Array.isArray(spn) && spn.length > 0;
953
+ const servicePatterns = [/^svc[_-]/i, /^sa[_-]/i, /service/i, /^sql/i, /^iis/i, /^app/i];
954
+ const matchesPattern = servicePatterns.some((p) => p.test(u.sAMAccountName || ''));
955
+
956
+ if (!hasSPN && !matchesPattern) return false;
957
+
958
+ // Must be enabled
959
+ if (!u.enabled) return false;
960
+
961
+ // Check if interactive logon is NOT denied
962
+ // A service account should have "Deny log on locally" and "Deny log on through RDP"
963
+ // We check if the account has logged on recently (indicating interactive use)
964
+ // or if it doesn't have restrictions that would prevent interactive logon
965
+
966
+ // If the account has adminCount=0 (not protected by SDProp) and has recent logons
967
+ // it's likely being used interactively
968
+ const lastLogonStr = u.lastLogon;
969
+ if (lastLogonStr) {
970
+ const lastLogon = new Date(lastLogonStr);
971
+ const daysSinceLogon = (Date.now() - lastLogon.getTime()) / (1000 * 60 * 60 * 24);
972
+ // If logged on in last 30 days, may be used interactively
973
+ if (daysSinceLogon < 30) {
974
+ return true;
975
+ }
976
+ }
977
+
978
+ // Also flag if password is set to never expire but account can logon interactively
979
+ const pwdNeverExpires = u.userAccountControl ? (u.userAccountControl & 0x10000) !== 0 : false;
980
+ const notDelegated = u.userAccountControl ? (u.userAccountControl & 0x100000) !== 0 : false;
981
+
982
+ // Service accounts with password never expires but NOT marked as "not delegated" are risky
983
+ return pwdNeverExpires && !notDelegated;
984
+ });
985
+
986
+ return {
987
+ type: 'SERVICE_ACCOUNT_INTERACTIVE',
988
+ severity: 'high',
989
+ category: 'accounts',
990
+ title: 'Service Account with Interactive Logon',
991
+ description:
992
+ 'Service accounts appear to allow or use interactive logon. Service accounts should be restricted to service-only authentication.',
993
+ count: affected.length,
994
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
995
+ details:
996
+ affected.length > 0
997
+ ? {
998
+ recommendation:
999
+ 'Apply "Deny log on locally" and "Deny log on through Remote Desktop Services" rights. Use gMSA where possible.',
1000
+ risks: [
1001
+ 'Interactive sessions leave credentials in memory (mimikatz target)',
1002
+ 'Increases attack surface for credential theft',
1003
+ 'May indicate misuse of service accounts',
1004
+ ],
1005
+ }
1006
+ : undefined,
1007
+ };
1008
+ }
1009
+
1010
+ /**
1011
+ * Detect all service account related vulnerabilities
1012
+ */
1013
+ export function detectServiceAccountVulnerabilities(users: ADUser[], includeDetails: boolean): Finding[] {
1014
+ return [
1015
+ detectServiceAccountWithSpn(users, includeDetails),
1016
+ detectServiceAccountNaming(users, includeDetails),
1017
+ detectServiceAccountOldPassword(users, includeDetails),
1018
+ detectServiceAccountPrivileged(users, includeDetails),
1019
+ detectServiceAccountNoPreauth(users, includeDetails),
1020
+ detectServiceAccountWeakEncryption(users, includeDetails),
1021
+ ].filter((finding) => finding.count > 0);
1022
+ }
1023
+
1024
+ /**
1025
+ * Detect accounts with directory replication rights (DCSync risk)
1026
+ *
1027
+ * Users with DS-Replication-Get-Changes and DS-Replication-Get-Changes-All
1028
+ * can perform DCSync attacks to extract password hashes.
1029
+ *
1030
+ * @param users - Array of AD users
1031
+ * @param includeDetails - Whether to include affected entity details
1032
+ * @returns Finding for REPLICA_DIRECTORY_CHANGES
1033
+ */
1034
+ export function detectReplicaDirectoryChanges(users: ADUser[], includeDetails: boolean): Finding {
1035
+ // This detection primarily works with ACL data, but we can check for
1036
+ // users in groups that typically have replication rights
1037
+ const replicationGroups = [
1038
+ 'Domain Controllers',
1039
+ 'Enterprise Domain Controllers',
1040
+ 'Administrators',
1041
+ 'Domain Admins',
1042
+ 'Enterprise Admins',
1043
+ ];
1044
+
1045
+ // Non-admin users that might have replication rights through delegation
1046
+ const affected = users.filter((u) => {
1047
+ if (!u.enabled || !u.memberOf) return false;
1048
+ // Check if user is in a group that shouldn't have replication rights
1049
+ // but description or other fields suggest replication permissions
1050
+ const rawDesc = (u as Record<string, unknown>)['description'];
1051
+ const description = typeof rawDesc === 'string' ? rawDesc : (Array.isArray(rawDesc) ? rawDesc[0] : '') || '';
1052
+ const hasReplicationHint =
1053
+ description.toLowerCase().includes('replication') ||
1054
+ description.toLowerCase().includes('dcsync') ||
1055
+ description.toLowerCase().includes('directory sync');
1056
+
1057
+ // Check for non-standard accounts with admin count (may have been delegated)
1058
+ const isServiceLike = /^(svc|service|sync|repl)/i.test(u.sAMAccountName);
1059
+ const hasAdminCount = u.adminCount === 1;
1060
+ const isInReplicationGroup = u.memberOf.some((dn) =>
1061
+ replicationGroups.some((g) => dn.toLowerCase().includes(g.toLowerCase()))
1062
+ );
1063
+
1064
+ return hasReplicationHint || (isServiceLike && hasAdminCount && !isInReplicationGroup);
1065
+ });
1066
+
1067
+ return {
1068
+ type: 'REPLICA_DIRECTORY_CHANGES',
1069
+ severity: 'critical',
1070
+ category: 'accounts',
1071
+ title: 'Potential Directory Replication Rights',
1072
+ description:
1073
+ 'Accounts that may have directory replication rights (DCSync capability). ' +
1074
+ 'These accounts can extract all password hashes from the domain.',
1075
+ count: affected.length,
1076
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
1077
+ details: {
1078
+ recommendation:
1079
+ 'Review ACLs on domain head for DS-Replication-Get-Changes rights. Only Domain Controllers should have this permission.',
1080
+ },
1081
+ };
1082
+ }
1083
+
1084
+ /**
1085
+ * Detect accounts in dangerous built-in groups
1086
+ *
1087
+ * Groups like Cert Publishers, RAS and IAS Servers, etc. have elevated privileges
1088
+ * that are often overlooked.
1089
+ *
1090
+ * @param users - Array of AD users
1091
+ * @param includeDetails - Whether to include affected entity details
1092
+ * @returns Finding for DANGEROUS_BUILTIN_MEMBERSHIP
1093
+ */
1094
+ export function detectDangerousBuiltinMembership(users: ADUser[], includeDetails: boolean): Finding {
1095
+ const dangerousGroups = [
1096
+ 'Cert Publishers', // Can publish certificates
1097
+ 'RAS and IAS Servers', // Network access
1098
+ 'Windows Authorization Access Group', // Token manipulation
1099
+ 'Terminal Server License Servers', // Remote access
1100
+ 'Incoming Forest Trust Builders', // Trust manipulation
1101
+ 'Performance Log Users', // Can access performance data
1102
+ 'Performance Monitor Users', // Can monitor system
1103
+ 'Distributed COM Users', // DCOM access
1104
+ 'Remote Desktop Users', // RDP access
1105
+ 'Network Configuration Operators', // Network config
1106
+ 'Cryptographic Operators', // Crypto operations
1107
+ 'Event Log Readers', // Security log access
1108
+ 'Hyper-V Administrators', // VM control
1109
+ 'Access Control Assistance Operators', // ACL modification
1110
+ 'Remote Management Users', // WinRM access
1111
+ ];
1112
+
1113
+ const affected = users.filter((u) => {
1114
+ if (!u.enabled || !u.memberOf) return false;
1115
+ return u.memberOf.some((dn) =>
1116
+ dangerousGroups.some((g) => dn.toLowerCase().includes(g.toLowerCase()))
1117
+ );
1118
+ });
1119
+
1120
+ return {
1121
+ type: 'DANGEROUS_BUILTIN_MEMBERSHIP',
1122
+ severity: 'medium',
1123
+ category: 'accounts',
1124
+ title: 'Dangerous Built-in Group Membership',
1125
+ description:
1126
+ 'User accounts with membership in overlooked but dangerous built-in groups. ' +
1127
+ 'These groups grant elevated privileges that may allow privilege escalation.',
1128
+ count: affected.length,
1129
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
1130
+ details: {
1131
+ dangerousGroups: dangerousGroups,
1132
+ },
1133
+ };
1134
+ }
1135
+
1136
+ /**
1137
+ * Detect locked admin accounts
1138
+ *
1139
+ * Administrative accounts that are currently locked may indicate
1140
+ * ongoing attack attempts or credential compromise.
1141
+ *
1142
+ * @param users - Array of AD users
1143
+ * @param includeDetails - Whether to include affected entity details
1144
+ * @returns Finding for LOCKED_ACCOUNT_ADMIN
1145
+ */
1146
+ export function detectLockedAccountAdmin(users: ADUser[], includeDetails: boolean): Finding {
1147
+ const adminGroups = [
1148
+ 'Domain Admins',
1149
+ 'Enterprise Admins',
1150
+ 'Schema Admins',
1151
+ 'Administrators',
1152
+ 'Account Operators',
1153
+ 'Server Operators',
1154
+ 'Backup Operators',
1155
+ ];
1156
+
1157
+ const affected = users.filter((u) => {
1158
+ if (!u.memberOf) return false;
1159
+ // Check if account is locked (lockoutTime != 0 or UAC flag 0x10)
1160
+ const isLocked =
1161
+ (u.lockoutTime && u.lockoutTime !== '0' && u.lockoutTime !== 0) ||
1162
+ (u.userAccountControl && (u.userAccountControl & 0x10) !== 0);
1163
+
1164
+ const isAdmin = u.memberOf.some((dn) =>
1165
+ adminGroups.some((g) => dn.toLowerCase().includes(g.toLowerCase()))
1166
+ );
1167
+
1168
+ return isLocked && isAdmin;
1169
+ });
1170
+
1171
+ return {
1172
+ type: 'LOCKED_ACCOUNT_ADMIN',
1173
+ severity: 'high',
1174
+ category: 'accounts',
1175
+ title: 'Locked Administrative Account',
1176
+ description:
1177
+ 'Administrative accounts that are currently locked out. ' +
1178
+ 'May indicate password spray attacks or compromised credential attempts.',
1179
+ count: affected.length,
1180
+ affectedEntities: includeDetails ? toAffectedUserEntities(affected) : undefined,
1181
+ details: {
1182
+ recommendation:
1183
+ 'Investigate why these admin accounts are locked. Check security logs for failed authentication attempts.',
1184
+ },
1185
+ };
1186
+ }
1187
+
1188
+ /**
1189
+ * Detect all account-related vulnerabilities
1190
+ */
1191
+ export function detectAccountsVulnerabilities(users: ADUser[], includeDetails: boolean): Finding[] {
1192
+ return [
1193
+ // Privileged accounts detectors
1194
+ detectSensitiveDelegation(users, includeDetails),
1195
+ detectDisabledAccountInAdminGroup(users, includeDetails),
1196
+ detectExpiredAccountInAdminGroup(users, includeDetails),
1197
+ detectSidHistory(users, includeDetails),
1198
+ detectNotInProtectedUsers(users, includeDetails),
1199
+ detectDomainAdminInDescription(users, includeDetails),
1200
+ detectBackupOperatorsMember(users, includeDetails),
1201
+ detectAccountOperatorsMember(users, includeDetails),
1202
+ detectServerOperatorsMember(users, includeDetails),
1203
+ detectPrintOperatorsMember(users, includeDetails),
1204
+ // Status detectors
1205
+ detectStaleAccount(users, includeDetails),
1206
+ detectInactive365Days(users, includeDetails),
1207
+ detectNeverLoggedOn(users, includeDetails),
1208
+ detectAccountExpireSoon(users, includeDetails),
1209
+ detectAdminLogonCountLow(users, includeDetails),
1210
+ // Dangerous patterns
1211
+ detectTestAccount(users, includeDetails),
1212
+ detectSharedAccount(users, includeDetails),
1213
+ detectSmartcardNotRequired(users, includeDetails),
1214
+ detectPrimaryGroupIdSpoofing(users, includeDetails),
1215
+ // Service account detectors
1216
+ detectServiceAccountWithSpn(users, includeDetails),
1217
+ detectServiceAccountNaming(users, includeDetails),
1218
+ detectServiceAccountOldPassword(users, includeDetails),
1219
+ detectServiceAccountPrivileged(users, includeDetails),
1220
+ detectServiceAccountNoPreauth(users, includeDetails),
1221
+ detectServiceAccountWeakEncryption(users, includeDetails),
1222
+ // Phase 2C: Enhanced detections
1223
+ detectAdminCountOrphaned(users, includeDetails),
1224
+ detectPrivilegedAccountSpn(users, includeDetails),
1225
+ detectAdminNoSmartcard(users, includeDetails),
1226
+ detectServiceAccountInteractive(users, includeDetails),
1227
+ // Phase 4: Advanced detections
1228
+ detectReplicaDirectoryChanges(users, includeDetails),
1229
+ detectDangerousBuiltinMembership(users, includeDetails),
1230
+ detectLockedAccountAdmin(users, includeDetails),
1231
+ ].filter((finding) => finding.count > 0);
1232
+ }