@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.
- package/.env.example +60 -0
- package/.env.test.example +33 -0
- package/.github/workflows/ci.yml +83 -0
- package/.github/workflows/release.yml +246 -0
- package/.prettierrc.json +10 -0
- package/CHANGELOG.md +15 -0
- package/Dockerfile +57 -0
- package/LICENSE +190 -0
- package/README.md +194 -0
- package/dist/api/controllers/audit.controller.d.ts +21 -0
- package/dist/api/controllers/audit.controller.d.ts.map +1 -0
- package/dist/api/controllers/audit.controller.js +179 -0
- package/dist/api/controllers/audit.controller.js.map +1 -0
- package/dist/api/controllers/auth.controller.d.ts +16 -0
- package/dist/api/controllers/auth.controller.d.ts.map +1 -0
- package/dist/api/controllers/auth.controller.js +146 -0
- package/dist/api/controllers/auth.controller.js.map +1 -0
- package/dist/api/controllers/export.controller.d.ts +27 -0
- package/dist/api/controllers/export.controller.d.ts.map +1 -0
- package/dist/api/controllers/export.controller.js +80 -0
- package/dist/api/controllers/export.controller.js.map +1 -0
- package/dist/api/controllers/health.controller.d.ts +5 -0
- package/dist/api/controllers/health.controller.d.ts.map +1 -0
- package/dist/api/controllers/health.controller.js +16 -0
- package/dist/api/controllers/health.controller.js.map +1 -0
- package/dist/api/controllers/jobs.controller.d.ts +13 -0
- package/dist/api/controllers/jobs.controller.d.ts.map +1 -0
- package/dist/api/controllers/jobs.controller.js +125 -0
- package/dist/api/controllers/jobs.controller.js.map +1 -0
- package/dist/api/controllers/providers.controller.d.ts +15 -0
- package/dist/api/controllers/providers.controller.d.ts.map +1 -0
- package/dist/api/controllers/providers.controller.js +112 -0
- package/dist/api/controllers/providers.controller.js.map +1 -0
- package/dist/api/dto/AuditRequest.dto.d.ts +6 -0
- package/dist/api/dto/AuditRequest.dto.d.ts.map +1 -0
- package/dist/api/dto/AuditRequest.dto.js +3 -0
- package/dist/api/dto/AuditRequest.dto.js.map +1 -0
- package/dist/api/dto/AuditResponse.dto.d.ts +17 -0
- package/dist/api/dto/AuditResponse.dto.d.ts.map +1 -0
- package/dist/api/dto/AuditResponse.dto.js +3 -0
- package/dist/api/dto/AuditResponse.dto.js.map +1 -0
- package/dist/api/dto/TokenRequest.dto.d.ts +6 -0
- package/dist/api/dto/TokenRequest.dto.d.ts.map +1 -0
- package/dist/api/dto/TokenRequest.dto.js +3 -0
- package/dist/api/dto/TokenRequest.dto.js.map +1 -0
- package/dist/api/dto/TokenResponse.dto.d.ts +12 -0
- package/dist/api/dto/TokenResponse.dto.d.ts.map +1 -0
- package/dist/api/dto/TokenResponse.dto.js +3 -0
- package/dist/api/dto/TokenResponse.dto.js.map +1 -0
- package/dist/api/middlewares/authenticate.d.ts +12 -0
- package/dist/api/middlewares/authenticate.d.ts.map +1 -0
- package/dist/api/middlewares/authenticate.js +141 -0
- package/dist/api/middlewares/authenticate.js.map +1 -0
- package/dist/api/middlewares/errorHandler.d.ts +3 -0
- package/dist/api/middlewares/errorHandler.d.ts.map +1 -0
- package/dist/api/middlewares/errorHandler.js +30 -0
- package/dist/api/middlewares/errorHandler.js.map +1 -0
- package/dist/api/middlewares/rateLimit.d.ts +3 -0
- package/dist/api/middlewares/rateLimit.d.ts.map +1 -0
- package/dist/api/middlewares/rateLimit.js +34 -0
- package/dist/api/middlewares/rateLimit.js.map +1 -0
- package/dist/api/middlewares/validate.d.ts +4 -0
- package/dist/api/middlewares/validate.d.ts.map +1 -0
- package/dist/api/middlewares/validate.js +31 -0
- package/dist/api/middlewares/validate.js.map +1 -0
- package/dist/api/routes/audit.routes.d.ts +5 -0
- package/dist/api/routes/audit.routes.d.ts.map +1 -0
- package/dist/api/routes/audit.routes.js +24 -0
- package/dist/api/routes/audit.routes.js.map +1 -0
- package/dist/api/routes/auth.routes.d.ts +6 -0
- package/dist/api/routes/auth.routes.d.ts.map +1 -0
- package/dist/api/routes/auth.routes.js +22 -0
- package/dist/api/routes/auth.routes.js.map +1 -0
- package/dist/api/routes/export.routes.d.ts +5 -0
- package/dist/api/routes/export.routes.d.ts.map +1 -0
- package/dist/api/routes/export.routes.js +16 -0
- package/dist/api/routes/export.routes.js.map +1 -0
- package/dist/api/routes/health.routes.d.ts +4 -0
- package/dist/api/routes/health.routes.d.ts.map +1 -0
- package/dist/api/routes/health.routes.js +11 -0
- package/dist/api/routes/health.routes.js.map +1 -0
- package/dist/api/routes/index.d.ts +10 -0
- package/dist/api/routes/index.d.ts.map +1 -0
- package/dist/api/routes/index.js +20 -0
- package/dist/api/routes/index.js.map +1 -0
- package/dist/api/routes/providers.routes.d.ts +5 -0
- package/dist/api/routes/providers.routes.d.ts.map +1 -0
- package/dist/api/routes/providers.routes.js +13 -0
- package/dist/api/routes/providers.routes.js.map +1 -0
- package/dist/api/validators/audit.schemas.d.ts +60 -0
- package/dist/api/validators/audit.schemas.d.ts.map +1 -0
- package/dist/api/validators/audit.schemas.js +55 -0
- package/dist/api/validators/audit.schemas.js.map +1 -0
- package/dist/api/validators/auth.schemas.d.ts +17 -0
- package/dist/api/validators/auth.schemas.d.ts.map +1 -0
- package/dist/api/validators/auth.schemas.js +21 -0
- package/dist/api/validators/auth.schemas.js.map +1 -0
- package/dist/app.d.ts +3 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +62 -0
- package/dist/app.js.map +1 -0
- package/dist/config/config.schema.d.ts +65 -0
- package/dist/config/config.schema.d.ts.map +1 -0
- package/dist/config/config.schema.js +95 -0
- package/dist/config/config.schema.js.map +1 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +75 -0
- package/dist/config/index.js.map +1 -0
- package/dist/container.d.ts +47 -0
- package/dist/container.d.ts.map +1 -0
- package/dist/container.js +137 -0
- package/dist/container.js.map +1 -0
- package/dist/data/database.d.ts +13 -0
- package/dist/data/database.d.ts.map +1 -0
- package/dist/data/database.js +68 -0
- package/dist/data/database.js.map +1 -0
- package/dist/data/jobs/token-cleanup.job.d.ts +23 -0
- package/dist/data/jobs/token-cleanup.job.d.ts.map +1 -0
- package/dist/data/jobs/token-cleanup.job.js +96 -0
- package/dist/data/jobs/token-cleanup.job.js.map +1 -0
- package/dist/data/migrations/migration.runner.d.ts +13 -0
- package/dist/data/migrations/migration.runner.d.ts.map +1 -0
- package/dist/data/migrations/migration.runner.js +136 -0
- package/dist/data/migrations/migration.runner.js.map +1 -0
- package/dist/data/models/Token.model.d.ts +30 -0
- package/dist/data/models/Token.model.d.ts.map +1 -0
- package/dist/data/models/Token.model.js +3 -0
- package/dist/data/models/Token.model.js.map +1 -0
- package/dist/data/repositories/token.repository.d.ts +16 -0
- package/dist/data/repositories/token.repository.d.ts.map +1 -0
- package/dist/data/repositories/token.repository.js +97 -0
- package/dist/data/repositories/token.repository.js.map +1 -0
- package/dist/providers/azure/auth.provider.d.ts +5 -0
- package/dist/providers/azure/auth.provider.d.ts.map +1 -0
- package/dist/providers/azure/auth.provider.js +13 -0
- package/dist/providers/azure/auth.provider.js.map +1 -0
- package/dist/providers/azure/azure-errors.d.ts +40 -0
- package/dist/providers/azure/azure-errors.d.ts.map +1 -0
- package/dist/providers/azure/azure-errors.js +121 -0
- package/dist/providers/azure/azure-errors.js.map +1 -0
- package/dist/providers/azure/azure-retry.d.ts +41 -0
- package/dist/providers/azure/azure-retry.d.ts.map +1 -0
- package/dist/providers/azure/azure-retry.js +85 -0
- package/dist/providers/azure/azure-retry.js.map +1 -0
- package/dist/providers/azure/graph-client.d.ts +26 -0
- package/dist/providers/azure/graph-client.d.ts.map +1 -0
- package/dist/providers/azure/graph-client.js +146 -0
- package/dist/providers/azure/graph-client.js.map +1 -0
- package/dist/providers/azure/graph.provider.d.ts +23 -0
- package/dist/providers/azure/graph.provider.d.ts.map +1 -0
- package/dist/providers/azure/graph.provider.js +161 -0
- package/dist/providers/azure/graph.provider.js.map +1 -0
- package/dist/providers/azure/queries/app.queries.d.ts +6 -0
- package/dist/providers/azure/queries/app.queries.d.ts.map +1 -0
- package/dist/providers/azure/queries/app.queries.js +9 -0
- package/dist/providers/azure/queries/app.queries.js.map +1 -0
- package/dist/providers/azure/queries/policy.queries.d.ts +6 -0
- package/dist/providers/azure/queries/policy.queries.d.ts.map +1 -0
- package/dist/providers/azure/queries/policy.queries.js +9 -0
- package/dist/providers/azure/queries/policy.queries.js.map +1 -0
- package/dist/providers/azure/queries/user.queries.d.ts +7 -0
- package/dist/providers/azure/queries/user.queries.d.ts.map +1 -0
- package/dist/providers/azure/queries/user.queries.js +10 -0
- package/dist/providers/azure/queries/user.queries.js.map +1 -0
- package/dist/providers/interfaces/IGraphProvider.d.ts +31 -0
- package/dist/providers/interfaces/IGraphProvider.d.ts.map +1 -0
- package/dist/providers/interfaces/IGraphProvider.js +3 -0
- package/dist/providers/interfaces/IGraphProvider.js.map +1 -0
- package/dist/providers/interfaces/ILDAPProvider.d.ts +37 -0
- package/dist/providers/interfaces/ILDAPProvider.d.ts.map +1 -0
- package/dist/providers/interfaces/ILDAPProvider.js +3 -0
- package/dist/providers/interfaces/ILDAPProvider.js.map +1 -0
- package/dist/providers/ldap/acl-parser.d.ts +8 -0
- package/dist/providers/ldap/acl-parser.d.ts.map +1 -0
- package/dist/providers/ldap/acl-parser.js +157 -0
- package/dist/providers/ldap/acl-parser.js.map +1 -0
- package/dist/providers/ldap/ad-mappers.d.ts +8 -0
- package/dist/providers/ldap/ad-mappers.d.ts.map +1 -0
- package/dist/providers/ldap/ad-mappers.js +162 -0
- package/dist/providers/ldap/ad-mappers.js.map +1 -0
- package/dist/providers/ldap/ldap-client.d.ts +33 -0
- package/dist/providers/ldap/ldap-client.d.ts.map +1 -0
- package/dist/providers/ldap/ldap-client.js +195 -0
- package/dist/providers/ldap/ldap-client.js.map +1 -0
- package/dist/providers/ldap/ldap-errors.d.ts +48 -0
- package/dist/providers/ldap/ldap-errors.d.ts.map +1 -0
- package/dist/providers/ldap/ldap-errors.js +120 -0
- package/dist/providers/ldap/ldap-errors.js.map +1 -0
- package/dist/providers/ldap/ldap-retry.d.ts +14 -0
- package/dist/providers/ldap/ldap-retry.d.ts.map +1 -0
- package/dist/providers/ldap/ldap-retry.js +102 -0
- package/dist/providers/ldap/ldap-retry.js.map +1 -0
- package/dist/providers/ldap/ldap-sanitizer.d.ts +12 -0
- package/dist/providers/ldap/ldap-sanitizer.d.ts.map +1 -0
- package/dist/providers/ldap/ldap-sanitizer.js +104 -0
- package/dist/providers/ldap/ldap-sanitizer.js.map +1 -0
- package/dist/providers/ldap/ldap.provider.d.ts +21 -0
- package/dist/providers/ldap/ldap.provider.d.ts.map +1 -0
- package/dist/providers/ldap/ldap.provider.js +165 -0
- package/dist/providers/ldap/ldap.provider.js.map +1 -0
- package/dist/providers/ldap/queries/computer.queries.d.ts +6 -0
- package/dist/providers/ldap/queries/computer.queries.d.ts.map +1 -0
- package/dist/providers/ldap/queries/computer.queries.js +9 -0
- package/dist/providers/ldap/queries/computer.queries.js.map +1 -0
- package/dist/providers/ldap/queries/group.queries.d.ts +6 -0
- package/dist/providers/ldap/queries/group.queries.d.ts.map +1 -0
- package/dist/providers/ldap/queries/group.queries.js +9 -0
- package/dist/providers/ldap/queries/group.queries.js.map +1 -0
- package/dist/providers/ldap/queries/user.queries.d.ts +7 -0
- package/dist/providers/ldap/queries/user.queries.d.ts.map +1 -0
- package/dist/providers/ldap/queries/user.queries.js +10 -0
- package/dist/providers/ldap/queries/user.queries.js.map +1 -0
- package/dist/providers/smb/smb.provider.d.ts +68 -0
- package/dist/providers/smb/smb.provider.d.ts.map +1 -0
- package/dist/providers/smb/smb.provider.js +382 -0
- package/dist/providers/smb/smb.provider.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +44 -0
- package/dist/server.js.map +1 -0
- package/dist/services/audit/ad-audit.service.d.ts +70 -0
- package/dist/services/audit/ad-audit.service.d.ts.map +1 -0
- package/dist/services/audit/ad-audit.service.js +1019 -0
- package/dist/services/audit/ad-audit.service.js.map +1 -0
- package/dist/services/audit/attack-graph.service.d.ts +62 -0
- package/dist/services/audit/attack-graph.service.d.ts.map +1 -0
- package/dist/services/audit/attack-graph.service.js +702 -0
- package/dist/services/audit/attack-graph.service.js.map +1 -0
- package/dist/services/audit/audit.service.d.ts +4 -0
- package/dist/services/audit/audit.service.d.ts.map +1 -0
- package/dist/services/audit/audit.service.js +10 -0
- package/dist/services/audit/audit.service.js.map +1 -0
- package/dist/services/audit/azure-audit.service.d.ts +37 -0
- package/dist/services/audit/azure-audit.service.d.ts.map +1 -0
- package/dist/services/audit/azure-audit.service.js +153 -0
- package/dist/services/audit/azure-audit.service.js.map +1 -0
- package/dist/services/audit/detectors/ad/accounts.detector.d.ts +37 -0
- package/dist/services/audit/detectors/ad/accounts.detector.d.ts.map +1 -0
- package/dist/services/audit/detectors/ad/accounts.detector.js +881 -0
- package/dist/services/audit/detectors/ad/accounts.detector.js.map +1 -0
- package/dist/services/audit/detectors/ad/adcs.detector.d.ts +21 -0
- package/dist/services/audit/detectors/ad/adcs.detector.d.ts.map +1 -0
- package/dist/services/audit/detectors/ad/adcs.detector.js +227 -0
- package/dist/services/audit/detectors/ad/adcs.detector.js.map +1 -0
- package/dist/services/audit/detectors/ad/advanced.detector.d.ts +63 -0
- package/dist/services/audit/detectors/ad/advanced.detector.d.ts.map +1 -0
- package/dist/services/audit/detectors/ad/advanced.detector.js +867 -0
- package/dist/services/audit/detectors/ad/advanced.detector.js.map +1 -0
- package/dist/services/audit/detectors/ad/attack-paths.detector.d.ts +16 -0
- package/dist/services/audit/detectors/ad/attack-paths.detector.d.ts.map +1 -0
- package/dist/services/audit/detectors/ad/attack-paths.detector.js +369 -0
- package/dist/services/audit/detectors/ad/attack-paths.detector.js.map +1 -0
- package/dist/services/audit/detectors/ad/compliance.detector.d.ts +28 -0
- package/dist/services/audit/detectors/ad/compliance.detector.d.ts.map +1 -0
- package/dist/services/audit/detectors/ad/compliance.detector.js +896 -0
- package/dist/services/audit/detectors/ad/compliance.detector.js.map +1 -0
- package/dist/services/audit/detectors/ad/computers.detector.d.ts +30 -0
- package/dist/services/audit/detectors/ad/computers.detector.d.ts.map +1 -0
- package/dist/services/audit/detectors/ad/computers.detector.js +799 -0
- package/dist/services/audit/detectors/ad/computers.detector.js.map +1 -0
- package/dist/services/audit/detectors/ad/gpo.detector.d.ts +17 -0
- package/dist/services/audit/detectors/ad/gpo.detector.d.ts.map +1 -0
- package/dist/services/audit/detectors/ad/gpo.detector.js +257 -0
- package/dist/services/audit/detectors/ad/gpo.detector.js.map +1 -0
- package/dist/services/audit/detectors/ad/groups.detector.d.ts +19 -0
- package/dist/services/audit/detectors/ad/groups.detector.d.ts.map +1 -0
- package/dist/services/audit/detectors/ad/groups.detector.js +488 -0
- package/dist/services/audit/detectors/ad/groups.detector.js.map +1 -0
- package/dist/services/audit/detectors/ad/index.d.ts +15 -0
- package/dist/services/audit/detectors/ad/index.d.ts.map +1 -0
- package/dist/services/audit/detectors/ad/index.js +51 -0
- package/dist/services/audit/detectors/ad/index.js.map +1 -0
- package/dist/services/audit/detectors/ad/kerberos.detector.d.ts +17 -0
- package/dist/services/audit/detectors/ad/kerberos.detector.d.ts.map +1 -0
- package/dist/services/audit/detectors/ad/kerberos.detector.js +293 -0
- package/dist/services/audit/detectors/ad/kerberos.detector.js.map +1 -0
- package/dist/services/audit/detectors/ad/monitoring.detector.d.ts +23 -0
- package/dist/services/audit/detectors/ad/monitoring.detector.d.ts.map +1 -0
- package/dist/services/audit/detectors/ad/monitoring.detector.js +328 -0
- package/dist/services/audit/detectors/ad/monitoring.detector.js.map +1 -0
- package/dist/services/audit/detectors/ad/network.detector.d.ts +39 -0
- package/dist/services/audit/detectors/ad/network.detector.d.ts.map +1 -0
- package/dist/services/audit/detectors/ad/network.detector.js +257 -0
- package/dist/services/audit/detectors/ad/network.detector.js.map +1 -0
- package/dist/services/audit/detectors/ad/password.detector.d.ts +14 -0
- package/dist/services/audit/detectors/ad/password.detector.d.ts.map +1 -0
- package/dist/services/audit/detectors/ad/password.detector.js +235 -0
- package/dist/services/audit/detectors/ad/password.detector.js.map +1 -0
- package/dist/services/audit/detectors/ad/permissions.detector.d.ts +20 -0
- package/dist/services/audit/detectors/ad/permissions.detector.d.ts.map +1 -0
- package/dist/services/audit/detectors/ad/permissions.detector.js +392 -0
- package/dist/services/audit/detectors/ad/permissions.detector.js.map +1 -0
- package/dist/services/audit/detectors/ad/trusts.detector.d.ts +11 -0
- package/dist/services/audit/detectors/ad/trusts.detector.d.ts.map +1 -0
- package/dist/services/audit/detectors/ad/trusts.detector.js +186 -0
- package/dist/services/audit/detectors/ad/trusts.detector.js.map +1 -0
- package/dist/services/audit/detectors/azure/app-security.detector.d.ts +11 -0
- package/dist/services/audit/detectors/azure/app-security.detector.d.ts.map +1 -0
- package/dist/services/audit/detectors/azure/app-security.detector.js +184 -0
- package/dist/services/audit/detectors/azure/app-security.detector.js.map +1 -0
- package/dist/services/audit/detectors/azure/conditional-access.detector.d.ts +10 -0
- package/dist/services/audit/detectors/azure/conditional-access.detector.d.ts.map +1 -0
- package/dist/services/audit/detectors/azure/conditional-access.detector.js +130 -0
- package/dist/services/audit/detectors/azure/conditional-access.detector.js.map +1 -0
- package/dist/services/audit/detectors/azure/privilege-security.detector.d.ts +8 -0
- package/dist/services/audit/detectors/azure/privilege-security.detector.d.ts.map +1 -0
- package/dist/services/audit/detectors/azure/privilege-security.detector.js +113 -0
- package/dist/services/audit/detectors/azure/privilege-security.detector.js.map +1 -0
- package/dist/services/audit/detectors/azure/user-security.detector.d.ts +14 -0
- package/dist/services/audit/detectors/azure/user-security.detector.d.ts.map +1 -0
- package/dist/services/audit/detectors/azure/user-security.detector.js +198 -0
- package/dist/services/audit/detectors/azure/user-security.detector.js.map +1 -0
- package/dist/services/audit/detectors/index.d.ts +2 -0
- package/dist/services/audit/detectors/index.d.ts.map +1 -0
- package/dist/services/audit/detectors/index.js +38 -0
- package/dist/services/audit/detectors/index.js.map +1 -0
- package/dist/services/audit/response-formatter.d.ts +176 -0
- package/dist/services/audit/response-formatter.d.ts.map +1 -0
- package/dist/services/audit/response-formatter.js +240 -0
- package/dist/services/audit/response-formatter.js.map +1 -0
- package/dist/services/audit/scoring.service.d.ts +15 -0
- package/dist/services/audit/scoring.service.d.ts.map +1 -0
- package/dist/services/audit/scoring.service.js +139 -0
- package/dist/services/audit/scoring.service.js.map +1 -0
- package/dist/services/auth/crypto.service.d.ts +19 -0
- package/dist/services/auth/crypto.service.d.ts.map +1 -0
- package/dist/services/auth/crypto.service.js +135 -0
- package/dist/services/auth/crypto.service.js.map +1 -0
- package/dist/services/auth/errors.d.ts +19 -0
- package/dist/services/auth/errors.d.ts.map +1 -0
- package/dist/services/auth/errors.js +46 -0
- package/dist/services/auth/errors.js.map +1 -0
- package/dist/services/auth/token.service.d.ts +41 -0
- package/dist/services/auth/token.service.d.ts.map +1 -0
- package/dist/services/auth/token.service.js +208 -0
- package/dist/services/auth/token.service.js.map +1 -0
- package/dist/services/config/config.service.d.ts +6 -0
- package/dist/services/config/config.service.d.ts.map +1 -0
- package/dist/services/config/config.service.js +64 -0
- package/dist/services/config/config.service.js.map +1 -0
- package/dist/services/export/export.service.d.ts +28 -0
- package/dist/services/export/export.service.d.ts.map +1 -0
- package/dist/services/export/export.service.js +28 -0
- package/dist/services/export/export.service.js.map +1 -0
- package/dist/services/export/formatters/csv.formatter.d.ts +8 -0
- package/dist/services/export/formatters/csv.formatter.d.ts.map +1 -0
- package/dist/services/export/formatters/csv.formatter.js +46 -0
- package/dist/services/export/formatters/csv.formatter.js.map +1 -0
- package/dist/services/export/formatters/json.formatter.d.ts +40 -0
- package/dist/services/export/formatters/json.formatter.d.ts.map +1 -0
- package/dist/services/export/formatters/json.formatter.js +58 -0
- package/dist/services/export/formatters/json.formatter.js.map +1 -0
- package/dist/services/jobs/azure-job-runner.d.ts +38 -0
- package/dist/services/jobs/azure-job-runner.d.ts.map +1 -0
- package/dist/services/jobs/azure-job-runner.js +199 -0
- package/dist/services/jobs/azure-job-runner.js.map +1 -0
- package/dist/services/jobs/index.d.ts +4 -0
- package/dist/services/jobs/index.d.ts.map +1 -0
- package/dist/services/jobs/index.js +20 -0
- package/dist/services/jobs/index.js.map +1 -0
- package/dist/services/jobs/job-runner.d.ts +64 -0
- package/dist/services/jobs/job-runner.d.ts.map +1 -0
- package/dist/services/jobs/job-runner.js +952 -0
- package/dist/services/jobs/job-runner.js.map +1 -0
- package/dist/services/jobs/job-store.d.ts +27 -0
- package/dist/services/jobs/job-store.d.ts.map +1 -0
- package/dist/services/jobs/job-store.js +261 -0
- package/dist/services/jobs/job-store.js.map +1 -0
- package/dist/services/jobs/job.types.d.ts +67 -0
- package/dist/services/jobs/job.types.d.ts.map +1 -0
- package/dist/services/jobs/job.types.js +36 -0
- package/dist/services/jobs/job.types.js.map +1 -0
- package/dist/types/ad.types.d.ts +74 -0
- package/dist/types/ad.types.d.ts.map +1 -0
- package/dist/types/ad.types.js +3 -0
- package/dist/types/ad.types.js.map +1 -0
- package/dist/types/adcs.types.d.ts +58 -0
- package/dist/types/adcs.types.d.ts.map +1 -0
- package/dist/types/adcs.types.js +38 -0
- package/dist/types/adcs.types.js.map +1 -0
- package/dist/types/attack-graph.types.d.ts +135 -0
- package/dist/types/attack-graph.types.d.ts.map +1 -0
- package/dist/types/attack-graph.types.js +58 -0
- package/dist/types/attack-graph.types.js.map +1 -0
- package/dist/types/audit.types.d.ts +34 -0
- package/dist/types/audit.types.d.ts.map +1 -0
- package/dist/types/audit.types.js +3 -0
- package/dist/types/audit.types.js.map +1 -0
- package/dist/types/azure.types.d.ts +61 -0
- package/dist/types/azure.types.d.ts.map +1 -0
- package/dist/types/azure.types.js +3 -0
- package/dist/types/azure.types.js.map +1 -0
- package/dist/types/config.types.d.ts +63 -0
- package/dist/types/config.types.d.ts.map +1 -0
- package/dist/types/config.types.js +3 -0
- package/dist/types/config.types.js.map +1 -0
- package/dist/types/error.types.d.ts +33 -0
- package/dist/types/error.types.d.ts.map +1 -0
- package/dist/types/error.types.js +70 -0
- package/dist/types/error.types.js.map +1 -0
- package/dist/types/finding.types.d.ts +133 -0
- package/dist/types/finding.types.d.ts.map +1 -0
- package/dist/types/finding.types.js +3 -0
- package/dist/types/finding.types.js.map +1 -0
- package/dist/types/gpo.types.d.ts +39 -0
- package/dist/types/gpo.types.d.ts.map +1 -0
- package/dist/types/gpo.types.js +15 -0
- package/dist/types/gpo.types.js.map +1 -0
- package/dist/types/token.types.d.ts +26 -0
- package/dist/types/token.types.d.ts.map +1 -0
- package/dist/types/token.types.js +3 -0
- package/dist/types/token.types.js.map +1 -0
- package/dist/types/trust.types.d.ts +45 -0
- package/dist/types/trust.types.d.ts.map +1 -0
- package/dist/types/trust.types.js +71 -0
- package/dist/types/trust.types.js.map +1 -0
- package/dist/utils/entity-converter.d.ts +17 -0
- package/dist/utils/entity-converter.d.ts.map +1 -0
- package/dist/utils/entity-converter.js +285 -0
- package/dist/utils/entity-converter.js.map +1 -0
- package/dist/utils/graph.util.d.ts +66 -0
- package/dist/utils/graph.util.d.ts.map +1 -0
- package/dist/utils/graph.util.js +382 -0
- package/dist/utils/graph.util.js.map +1 -0
- package/dist/utils/logger.d.ts +7 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +86 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/type-name-normalizer.d.ts +5 -0
- package/dist/utils/type-name-normalizer.d.ts.map +1 -0
- package/dist/utils/type-name-normalizer.js +218 -0
- package/dist/utils/type-name-normalizer.js.map +1 -0
- package/docker-compose.yml +26 -0
- package/docs/api/README.md +178 -0
- package/docs/api/openapi.yaml +1524 -0
- package/eslint.config.js +54 -0
- package/jest.config.js +38 -0
- package/package.json +97 -0
- package/scripts/fetch-ad-cert.sh +142 -0
- package/src/.gitkeep +0 -0
- package/src/api/.gitkeep +0 -0
- package/src/api/controllers/.gitkeep +0 -0
- package/src/api/controllers/audit.controller.ts +313 -0
- package/src/api/controllers/auth.controller.ts +258 -0
- package/src/api/controllers/export.controller.ts +153 -0
- package/src/api/controllers/health.controller.ts +16 -0
- package/src/api/controllers/jobs.controller.ts +187 -0
- package/src/api/controllers/providers.controller.ts +165 -0
- package/src/api/dto/.gitkeep +0 -0
- package/src/api/dto/AuditRequest.dto.ts +8 -0
- package/src/api/dto/AuditResponse.dto.ts +19 -0
- package/src/api/dto/TokenRequest.dto.ts +8 -0
- package/src/api/dto/TokenResponse.dto.ts +14 -0
- package/src/api/middlewares/.gitkeep +0 -0
- package/src/api/middlewares/authenticate.ts +203 -0
- package/src/api/middlewares/errorHandler.ts +54 -0
- package/src/api/middlewares/rateLimit.ts +35 -0
- package/src/api/middlewares/validate.ts +32 -0
- package/src/api/routes/.gitkeep +0 -0
- package/src/api/routes/audit.routes.ts +77 -0
- package/src/api/routes/auth.routes.ts +71 -0
- package/src/api/routes/export.routes.ts +34 -0
- package/src/api/routes/health.routes.ts +14 -0
- package/src/api/routes/index.ts +40 -0
- package/src/api/routes/providers.routes.ts +24 -0
- package/src/api/validators/.gitkeep +0 -0
- package/src/api/validators/audit.schemas.ts +59 -0
- package/src/api/validators/auth.schemas.ts +59 -0
- package/src/app.ts +87 -0
- package/src/config/.gitkeep +0 -0
- package/src/config/config.schema.ts +108 -0
- package/src/config/index.ts +82 -0
- package/src/container.ts +221 -0
- package/src/data/.gitkeep +0 -0
- package/src/data/database.ts +78 -0
- package/src/data/jobs/token-cleanup.job.ts +166 -0
- package/src/data/migrations/.gitkeep +0 -0
- package/src/data/migrations/001_initial_schema.sql +47 -0
- package/src/data/migrations/migration.runner.ts +125 -0
- package/src/data/models/.gitkeep +0 -0
- package/src/data/models/Token.model.ts +35 -0
- package/src/data/repositories/.gitkeep +0 -0
- package/src/data/repositories/token.repository.ts +160 -0
- package/src/providers/.gitkeep +0 -0
- package/src/providers/azure/.gitkeep +0 -0
- package/src/providers/azure/auth.provider.ts +14 -0
- package/src/providers/azure/azure-errors.ts +189 -0
- package/src/providers/azure/azure-retry.ts +168 -0
- package/src/providers/azure/graph-client.ts +315 -0
- package/src/providers/azure/graph.provider.ts +294 -0
- package/src/providers/azure/queries/app.queries.ts +9 -0
- package/src/providers/azure/queries/policy.queries.ts +9 -0
- package/src/providers/azure/queries/user.queries.ts +10 -0
- package/src/providers/interfaces/.gitkeep +0 -0
- package/src/providers/interfaces/IGraphProvider.ts +117 -0
- package/src/providers/interfaces/ILDAPProvider.ts +142 -0
- package/src/providers/ldap/.gitkeep +0 -0
- package/src/providers/ldap/acl-parser.ts +231 -0
- package/src/providers/ldap/ad-mappers.ts +280 -0
- package/src/providers/ldap/ldap-client.ts +259 -0
- package/src/providers/ldap/ldap-errors.ts +188 -0
- package/src/providers/ldap/ldap-retry.ts +267 -0
- package/src/providers/ldap/ldap-sanitizer.ts +273 -0
- package/src/providers/ldap/ldap.provider.ts +293 -0
- package/src/providers/ldap/queries/computer.queries.ts +9 -0
- package/src/providers/ldap/queries/group.queries.ts +9 -0
- package/src/providers/ldap/queries/user.queries.ts +10 -0
- package/src/providers/smb/smb.provider.ts +653 -0
- package/src/server.ts +60 -0
- package/src/services/.gitkeep +0 -0
- package/src/services/audit/.gitkeep +0 -0
- package/src/services/audit/ad-audit.service.ts +1481 -0
- package/src/services/audit/attack-graph.service.ts +1104 -0
- package/src/services/audit/audit.service.ts +12 -0
- package/src/services/audit/azure-audit.service.ts +286 -0
- package/src/services/audit/detectors/ad/accounts.detector.ts +1232 -0
- package/src/services/audit/detectors/ad/adcs.detector.ts +449 -0
- package/src/services/audit/detectors/ad/advanced.detector.ts +1270 -0
- package/src/services/audit/detectors/ad/attack-paths.detector.ts +600 -0
- package/src/services/audit/detectors/ad/compliance.detector.ts +1421 -0
- package/src/services/audit/detectors/ad/computers.detector.ts +1188 -0
- package/src/services/audit/detectors/ad/gpo.detector.ts +485 -0
- package/src/services/audit/detectors/ad/groups.detector.ts +685 -0
- package/src/services/audit/detectors/ad/index.ts +84 -0
- package/src/services/audit/detectors/ad/kerberos.detector.ts +424 -0
- package/src/services/audit/detectors/ad/monitoring.detector.ts +501 -0
- package/src/services/audit/detectors/ad/network.detector.ts +538 -0
- package/src/services/audit/detectors/ad/password.detector.ts +324 -0
- package/src/services/audit/detectors/ad/permissions.detector.ts +637 -0
- package/src/services/audit/detectors/ad/trusts.detector.ts +315 -0
- package/src/services/audit/detectors/azure/app-security.detector.ts +246 -0
- package/src/services/audit/detectors/azure/conditional-access.detector.ts +186 -0
- package/src/services/audit/detectors/azure/privilege-security.detector.ts +176 -0
- package/src/services/audit/detectors/azure/user-security.detector.ts +280 -0
- package/src/services/audit/detectors/index.ts +18 -0
- package/src/services/audit/response-formatter.ts +604 -0
- package/src/services/audit/scoring.service.ts +234 -0
- package/src/services/auth/.gitkeep +0 -0
- package/src/services/auth/crypto.service.ts +230 -0
- package/src/services/auth/errors.ts +47 -0
- package/src/services/auth/token.service.ts +420 -0
- package/src/services/config/.gitkeep +0 -0
- package/src/services/config/config.service.ts +75 -0
- package/src/services/export/.gitkeep +0 -0
- package/src/services/export/export.service.ts +99 -0
- package/src/services/export/formatters/csv.formatter.ts +124 -0
- package/src/services/export/formatters/json.formatter.ts +160 -0
- package/src/services/jobs/azure-job-runner.ts +312 -0
- package/src/services/jobs/index.ts +9 -0
- package/src/services/jobs/job-runner.ts +1280 -0
- package/src/services/jobs/job-store.ts +384 -0
- package/src/services/jobs/job.types.ts +182 -0
- package/src/types/.gitkeep +0 -0
- package/src/types/ad.types.ts +91 -0
- package/src/types/adcs.types.ts +107 -0
- package/src/types/attack-graph.types.ts +260 -0
- package/src/types/audit.types.ts +42 -0
- package/src/types/azure.types.ts +68 -0
- package/src/types/config.types.ts +79 -0
- package/src/types/error.types.ts +69 -0
- package/src/types/finding.types.ts +284 -0
- package/src/types/gpo.types.ts +72 -0
- package/src/types/smb2.d.ts +73 -0
- package/src/types/token.types.ts +32 -0
- package/src/types/trust.types.ts +140 -0
- package/src/utils/.gitkeep +0 -0
- package/src/utils/entity-converter.ts +453 -0
- package/src/utils/graph.util.ts +609 -0
- package/src/utils/logger.ts +111 -0
- package/src/utils/type-name-normalizer.ts +302 -0
- package/tests/.gitkeep +0 -0
- package/tests/e2e/.gitkeep +0 -0
- package/tests/fixtures/.gitkeep +0 -0
- package/tests/integration/.gitkeep +0 -0
- package/tests/integration/README.md +156 -0
- package/tests/integration/ad-audit.integration.test.ts +216 -0
- package/tests/integration/api/.gitkeep +0 -0
- package/tests/integration/api/endpoints.integration.test.ts +431 -0
- package/tests/integration/auth/jwt-authentication.integration.test.ts +358 -0
- package/tests/integration/providers/.gitkeep +0 -0
- package/tests/integration/providers/azure-basic.integration.test.ts +167 -0
- package/tests/integration/providers/ldap-basic.integration.test.ts +152 -0
- package/tests/integration/providers/ldap-connectivity.test.ts +44 -0
- package/tests/integration/providers/ldap-provider.integration.test.ts +347 -0
- package/tests/mocks/.gitkeep +0 -0
- package/tests/setup.ts +16 -0
- package/tests/unit/.gitkeep +0 -0
- package/tests/unit/api/middlewares/authenticate.test.ts +446 -0
- package/tests/unit/providers/.gitkeep +0 -0
- package/tests/unit/providers/azure/azure-errors.test.ts +193 -0
- package/tests/unit/providers/azure/azure-retry.test.ts +254 -0
- package/tests/unit/providers/azure/graph-provider.test.ts +313 -0
- package/tests/unit/providers/ldap/ad-mappers.test.ts +392 -0
- package/tests/unit/providers/ldap/ldap-provider.test.ts +376 -0
- package/tests/unit/providers/ldap/ldap-retry.test.ts +377 -0
- package/tests/unit/providers/ldap/ldap-sanitizer.test.ts +301 -0
- package/tests/unit/sample.test.ts +19 -0
- package/tests/unit/services/.gitkeep +0 -0
- package/tests/unit/services/audit/detectors/ad/accounts.detector.test.ts +393 -0
- package/tests/unit/services/audit/detectors/ad/advanced.detector.test.ts +380 -0
- package/tests/unit/services/audit/detectors/ad/computers.detector.test.ts +440 -0
- package/tests/unit/services/audit/detectors/ad/groups.detector.test.ts +276 -0
- package/tests/unit/services/audit/detectors/ad/kerberos.detector.test.ts +215 -0
- package/tests/unit/services/audit/detectors/ad/password.detector.test.ts +226 -0
- package/tests/unit/services/audit/detectors/ad/permissions.detector.test.ts +244 -0
- package/tests/unit/services/audit/detectors/azure/app-security.detector.test.ts +349 -0
- package/tests/unit/services/audit/detectors/azure/conditional-access.detector.test.ts +374 -0
- package/tests/unit/services/audit/detectors/azure/privilege-security.detector.test.ts +374 -0
- package/tests/unit/services/audit/detectors/azure/user-security.detector.test.ts +297 -0
- package/tests/unit/services/auth/crypto.service.test.ts +296 -0
- package/tests/unit/services/auth/token.service.test.ts +579 -0
- package/tests/unit/services/export/export.service.test.ts +241 -0
- package/tests/unit/services/export/formatters/csv.formatter.test.ts +270 -0
- package/tests/unit/services/export/formatters/json.formatter.test.ts +258 -0
- package/tests/unit/utils/.gitkeep +0 -0
- 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
|
+
}
|