@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,1104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attack Graph Service
|
|
3
|
+
*
|
|
4
|
+
* Builds and exports attack path data for visualization.
|
|
5
|
+
* Uses BFS to find shortest paths from non-privileged objects
|
|
6
|
+
* to privileged targets (Domain Admins, Enterprise Admins, etc.)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ADUser, ADGroup, ADComputer, AclEntry } from '../../types/ad.types';
|
|
10
|
+
import { ADCSCertificateTemplate } from '../../types/adcs.types';
|
|
11
|
+
import { ADGPO } from '../../types/gpo.types';
|
|
12
|
+
import {
|
|
13
|
+
AttackGraphExport,
|
|
14
|
+
AttackGraphNode,
|
|
15
|
+
AttackGraphRelation,
|
|
16
|
+
AttackPath,
|
|
17
|
+
AttackPathRisk,
|
|
18
|
+
AttackPathType,
|
|
19
|
+
AttackRelationType,
|
|
20
|
+
AttackTarget,
|
|
21
|
+
AttackGraphStats,
|
|
22
|
+
AttackGraphUniqueNode,
|
|
23
|
+
AttackChainElement,
|
|
24
|
+
AttackEntryPoint,
|
|
25
|
+
AttackNodeType,
|
|
26
|
+
ACCESS_MASK,
|
|
27
|
+
ACL_GUIDS,
|
|
28
|
+
PRIVILEGED_SID_SUFFIXES,
|
|
29
|
+
isAttackGraphNode,
|
|
30
|
+
} from '../../types/attack-graph.types';
|
|
31
|
+
import { logger } from '../../utils/logger';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Internal graph node with additional metadata
|
|
35
|
+
*/
|
|
36
|
+
interface InternalNode {
|
|
37
|
+
id: string; // SID or DN-based ID
|
|
38
|
+
dn: string;
|
|
39
|
+
name: string;
|
|
40
|
+
type: AttackNodeType;
|
|
41
|
+
sid?: string;
|
|
42
|
+
isEnabled: boolean;
|
|
43
|
+
isPrivileged: boolean;
|
|
44
|
+
adminCount?: number;
|
|
45
|
+
memberOf: string[];
|
|
46
|
+
servicePrincipalName?: string[];
|
|
47
|
+
userAccountControl?: number;
|
|
48
|
+
delegateTo?: string[];
|
|
49
|
+
rbcdFrom?: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Internal graph edge
|
|
54
|
+
*/
|
|
55
|
+
interface InternalEdge {
|
|
56
|
+
source: string; // Node ID
|
|
57
|
+
target: string; // Node ID
|
|
58
|
+
relation: AttackRelationType;
|
|
59
|
+
accessMask?: number;
|
|
60
|
+
objectType?: string;
|
|
61
|
+
isAbusable: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* BFS path result
|
|
66
|
+
*/
|
|
67
|
+
interface BFSPath {
|
|
68
|
+
nodes: string[]; // Node IDs in order
|
|
69
|
+
edges: InternalEdge[]; // Edges between nodes
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Attack Graph Service
|
|
74
|
+
*/
|
|
75
|
+
export class AttackGraphService {
|
|
76
|
+
private nodes: Map<string, InternalNode> = new Map();
|
|
77
|
+
private edges: Map<string, InternalEdge[]> = new Map(); // source -> edges
|
|
78
|
+
private reverseEdges: Map<string, InternalEdge[]> = new Map(); // target -> edges
|
|
79
|
+
private privilegedTargets: Map<string, AttackTarget> = new Map();
|
|
80
|
+
private dnToId: Map<string, string> = new Map(); // DN -> node ID
|
|
81
|
+
private domainSid: string = '';
|
|
82
|
+
private domainName: string = '';
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Initialize the attack graph service with AD data
|
|
86
|
+
*/
|
|
87
|
+
constructor(
|
|
88
|
+
private users: ADUser[],
|
|
89
|
+
private groups: ADGroup[],
|
|
90
|
+
private computers: ADComputer[],
|
|
91
|
+
private aclEntries: AclEntry[],
|
|
92
|
+
_certTemplates: ADCSCertificateTemplate[] = [],
|
|
93
|
+
_gpos: ADGPO[] = [],
|
|
94
|
+
domain?: { name?: string; sid?: string }
|
|
95
|
+
) {
|
|
96
|
+
this.domainName = domain?.name || '';
|
|
97
|
+
this.domainSid = domain?.sid || this.extractDomainSid();
|
|
98
|
+
logger.debug(`AttackGraphService initialized with domain SID: ${this.domainSid}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Build and export the attack graph
|
|
103
|
+
*/
|
|
104
|
+
public export(maxPaths: number = 500): AttackGraphExport {
|
|
105
|
+
const startTime = Date.now();
|
|
106
|
+
|
|
107
|
+
// Build the graph
|
|
108
|
+
this.buildGraph();
|
|
109
|
+
|
|
110
|
+
// Identify privileged targets
|
|
111
|
+
this.identifyPrivilegedTargets();
|
|
112
|
+
|
|
113
|
+
// Find all attack paths
|
|
114
|
+
const paths = this.findAllAttackPaths(maxPaths);
|
|
115
|
+
|
|
116
|
+
// Compute statistics
|
|
117
|
+
const stats = this.computeStats(paths);
|
|
118
|
+
|
|
119
|
+
// Get unique nodes involved in paths
|
|
120
|
+
const uniqueNodes = this.getUniqueNodes(paths);
|
|
121
|
+
|
|
122
|
+
const duration = Date.now() - startTime;
|
|
123
|
+
logger.info(
|
|
124
|
+
`Attack graph exported: ${paths.length} paths, ${uniqueNodes.length} unique nodes in ${duration}ms`
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
version: '1.0',
|
|
129
|
+
generatedAt: new Date().toISOString(),
|
|
130
|
+
domain: this.domainName,
|
|
131
|
+
targets: Array.from(this.privilegedTargets.values()),
|
|
132
|
+
paths,
|
|
133
|
+
stats,
|
|
134
|
+
uniqueNodes,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Build the internal graph from AD data
|
|
140
|
+
*/
|
|
141
|
+
private buildGraph(): void {
|
|
142
|
+
// Add all nodes
|
|
143
|
+
this.addUserNodes();
|
|
144
|
+
this.addGroupNodes();
|
|
145
|
+
this.addComputerNodes();
|
|
146
|
+
|
|
147
|
+
// Add all edges
|
|
148
|
+
this.addMembershipEdges();
|
|
149
|
+
this.addAclEdges();
|
|
150
|
+
this.addDelegationEdges();
|
|
151
|
+
this.addKerberosEdges();
|
|
152
|
+
|
|
153
|
+
logger.debug(
|
|
154
|
+
`Graph built: ${this.nodes.size} nodes, ${this.countEdges()} edges`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Add user nodes to the graph
|
|
160
|
+
*/
|
|
161
|
+
private addUserNodes(): void {
|
|
162
|
+
for (const user of this.users) {
|
|
163
|
+
const sid = this.extractSid(user);
|
|
164
|
+
const id = sid || this.dnToId.get(user.dn.toLowerCase()) || this.generateId(user.dn);
|
|
165
|
+
|
|
166
|
+
const node: InternalNode = {
|
|
167
|
+
id,
|
|
168
|
+
dn: user.dn,
|
|
169
|
+
name: user.sAMAccountName || user.displayName || user.dn,
|
|
170
|
+
type: 'user',
|
|
171
|
+
sid,
|
|
172
|
+
isEnabled: user.enabled,
|
|
173
|
+
isPrivileged: user.adminCount === 1,
|
|
174
|
+
adminCount: user.adminCount,
|
|
175
|
+
memberOf: user.memberOf || [],
|
|
176
|
+
servicePrincipalName: this.getSpn(user),
|
|
177
|
+
userAccountControl: user.userAccountControl,
|
|
178
|
+
delegateTo: this.getDelegationTargets(user),
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
this.nodes.set(id, node);
|
|
182
|
+
this.dnToId.set(user.dn.toLowerCase(), id);
|
|
183
|
+
this.edges.set(id, []);
|
|
184
|
+
this.reverseEdges.set(id, []);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Add group nodes to the graph
|
|
190
|
+
*/
|
|
191
|
+
private addGroupNodes(): void {
|
|
192
|
+
for (const group of this.groups) {
|
|
193
|
+
const sid = this.extractSid(group);
|
|
194
|
+
const id = sid || this.generateId(group.dn);
|
|
195
|
+
|
|
196
|
+
// Check if this is a privileged group by SID
|
|
197
|
+
const isPrivileged = this.isPrivilegedBySid(sid);
|
|
198
|
+
|
|
199
|
+
const node: InternalNode = {
|
|
200
|
+
id,
|
|
201
|
+
dn: group.dn,
|
|
202
|
+
name: group.sAMAccountName || group.displayName || group.cn || group.dn,
|
|
203
|
+
type: 'group',
|
|
204
|
+
sid,
|
|
205
|
+
isEnabled: true,
|
|
206
|
+
isPrivileged,
|
|
207
|
+
memberOf: group.memberOf || [],
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
this.nodes.set(id, node);
|
|
211
|
+
this.dnToId.set(group.dn.toLowerCase(), id);
|
|
212
|
+
this.edges.set(id, []);
|
|
213
|
+
this.reverseEdges.set(id, []);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Add computer nodes to the graph
|
|
219
|
+
*/
|
|
220
|
+
private addComputerNodes(): void {
|
|
221
|
+
for (const computer of this.computers) {
|
|
222
|
+
const sid = this.extractSid(computer);
|
|
223
|
+
const id = sid || this.generateId(computer.dn);
|
|
224
|
+
|
|
225
|
+
// Check if Domain Controller
|
|
226
|
+
const memberOf = (computer as any).memberOf || [];
|
|
227
|
+
const isDC = memberOf.some(
|
|
228
|
+
(m: string) =>
|
|
229
|
+
m.toLowerCase().includes('cn=domain controllers') ||
|
|
230
|
+
m.toLowerCase().includes('cn=contrôleurs de domaine')
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const node: InternalNode = {
|
|
234
|
+
id,
|
|
235
|
+
dn: computer.dn,
|
|
236
|
+
name: computer.sAMAccountName || computer.dNSHostName || computer.cn || computer.dn,
|
|
237
|
+
type: 'computer',
|
|
238
|
+
sid,
|
|
239
|
+
isEnabled: computer.enabled,
|
|
240
|
+
isPrivileged: isDC,
|
|
241
|
+
memberOf: memberOf,
|
|
242
|
+
delegateTo: this.getDelegationTargets(computer),
|
|
243
|
+
rbcdFrom: this.getRbcdPrincipals(computer),
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
this.nodes.set(id, node);
|
|
247
|
+
this.dnToId.set(computer.dn.toLowerCase(), id);
|
|
248
|
+
this.edges.set(id, []);
|
|
249
|
+
this.reverseEdges.set(id, []);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Add membership edges (MemberOf)
|
|
255
|
+
*/
|
|
256
|
+
private addMembershipEdges(): void {
|
|
257
|
+
for (const [nodeId, node] of this.nodes) {
|
|
258
|
+
for (const groupDn of node.memberOf) {
|
|
259
|
+
const targetId = this.dnToId.get(groupDn.toLowerCase());
|
|
260
|
+
if (targetId) {
|
|
261
|
+
this.addEdge({
|
|
262
|
+
source: nodeId,
|
|
263
|
+
target: targetId,
|
|
264
|
+
relation: 'MemberOf',
|
|
265
|
+
isAbusable: false, // Membership itself isn't abusable, but paths through it are
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Add ACL-based edges
|
|
274
|
+
*/
|
|
275
|
+
private addAclEdges(): void {
|
|
276
|
+
for (const acl of this.aclEntries) {
|
|
277
|
+
// Skip denied ACEs
|
|
278
|
+
if (acl.aceType !== '0' && acl.aceType !== 'ACCESS_ALLOWED_ACE_TYPE') {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const sourceId = this.resolveAclTrustee(acl.trustee);
|
|
283
|
+
const targetId = this.dnToId.get(acl.objectDn.toLowerCase());
|
|
284
|
+
|
|
285
|
+
if (!sourceId || !targetId) continue;
|
|
286
|
+
|
|
287
|
+
// Determine relation type based on access mask and object type
|
|
288
|
+
const relations = this.classifyAclRelation(acl);
|
|
289
|
+
|
|
290
|
+
for (const relation of relations) {
|
|
291
|
+
this.addEdge({
|
|
292
|
+
source: sourceId,
|
|
293
|
+
target: targetId,
|
|
294
|
+
relation: relation.type,
|
|
295
|
+
accessMask: acl.accessMask,
|
|
296
|
+
objectType: acl.objectType,
|
|
297
|
+
isAbusable: relation.isAbusable,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Add delegation edges
|
|
305
|
+
*/
|
|
306
|
+
private addDelegationEdges(): void {
|
|
307
|
+
for (const [nodeId, node] of this.nodes) {
|
|
308
|
+
// Constrained delegation
|
|
309
|
+
if (node.delegateTo && node.delegateTo.length > 0) {
|
|
310
|
+
for (const spn of node.delegateTo) {
|
|
311
|
+
// Try to resolve SPN to a target
|
|
312
|
+
const targetId = this.resolveSPNTarget(spn);
|
|
313
|
+
if (targetId) {
|
|
314
|
+
this.addEdge({
|
|
315
|
+
source: nodeId,
|
|
316
|
+
target: targetId,
|
|
317
|
+
relation: 'AllowedToDelegate',
|
|
318
|
+
isAbusable: true,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// RBCD - reverse direction: those who can act get edge TO this computer
|
|
325
|
+
if (node.rbcdFrom && node.rbcdFrom.length > 0) {
|
|
326
|
+
for (const principalSid of node.rbcdFrom) {
|
|
327
|
+
const sourceId = this.nodes.has(principalSid)
|
|
328
|
+
? principalSid
|
|
329
|
+
: this.findNodeBySid(principalSid);
|
|
330
|
+
if (sourceId) {
|
|
331
|
+
this.addEdge({
|
|
332
|
+
source: sourceId,
|
|
333
|
+
target: nodeId,
|
|
334
|
+
relation: 'AllowedToAct',
|
|
335
|
+
isAbusable: true,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Add Kerberos-related edges (HasSPN, NoPreauth)
|
|
345
|
+
* Note: These are tracked as node properties for path classification
|
|
346
|
+
*/
|
|
347
|
+
private addKerberosEdges(): void {
|
|
348
|
+
// HasSPN and NoPreauth are tracked as node properties, not edges
|
|
349
|
+
// They are used during path classification to determine path type
|
|
350
|
+
// (KERBEROASTING, ASREP_ROASTING)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Identify privileged targets
|
|
355
|
+
*/
|
|
356
|
+
private identifyPrivilegedTargets(): void {
|
|
357
|
+
for (const [nodeId, node] of this.nodes) {
|
|
358
|
+
let reason: string | null = null;
|
|
359
|
+
|
|
360
|
+
// Check SID-based privileged groups
|
|
361
|
+
if (node.sid) {
|
|
362
|
+
if (node.sid.endsWith(PRIVILEGED_SID_SUFFIXES.DOMAIN_ADMINS)) {
|
|
363
|
+
reason = 'Domain Admins';
|
|
364
|
+
} else if (node.sid.endsWith(PRIVILEGED_SID_SUFFIXES.ENTERPRISE_ADMINS)) {
|
|
365
|
+
reason = 'Enterprise Admins';
|
|
366
|
+
} else if (node.sid.endsWith(PRIVILEGED_SID_SUFFIXES.SCHEMA_ADMINS)) {
|
|
367
|
+
reason = 'Schema Admins';
|
|
368
|
+
} else if (node.sid.endsWith(PRIVILEGED_SID_SUFFIXES.ADMINISTRATORS)) {
|
|
369
|
+
reason = 'Administrators';
|
|
370
|
+
} else if (node.sid.endsWith(PRIVILEGED_SID_SUFFIXES.DOMAIN_CONTROLLERS)) {
|
|
371
|
+
reason = 'Domain Controllers';
|
|
372
|
+
} else if (node.sid.endsWith(PRIVILEGED_SID_SUFFIXES.ACCOUNT_OPERATORS)) {
|
|
373
|
+
reason = 'Account Operators';
|
|
374
|
+
} else if (node.sid.endsWith(PRIVILEGED_SID_SUFFIXES.BACKUP_OPERATORS)) {
|
|
375
|
+
reason = 'Backup Operators';
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Check adminCount=1
|
|
380
|
+
if (!reason && node.adminCount === 1) {
|
|
381
|
+
reason = 'adminCount=1';
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Check name-based privileged groups
|
|
385
|
+
if (!reason && node.type === 'group') {
|
|
386
|
+
const nameLower = node.name.toLowerCase();
|
|
387
|
+
if (
|
|
388
|
+
nameLower === 'domain admins' ||
|
|
389
|
+
nameLower === 'admins du domaine' ||
|
|
390
|
+
nameLower === 'administrateurs du domaine'
|
|
391
|
+
) {
|
|
392
|
+
reason = 'Domain Admins';
|
|
393
|
+
} else if (
|
|
394
|
+
nameLower === 'enterprise admins' ||
|
|
395
|
+
nameLower === 'administrateurs de l\'entreprise'
|
|
396
|
+
) {
|
|
397
|
+
reason = 'Enterprise Admins';
|
|
398
|
+
} else if (nameLower === 'administrators' || nameLower === 'administrateurs') {
|
|
399
|
+
reason = 'Administrators';
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Check Domain Controller computers
|
|
404
|
+
if (!reason && node.type === 'computer' && node.isPrivileged) {
|
|
405
|
+
reason = 'Domain Controller';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (reason) {
|
|
409
|
+
node.isPrivileged = true;
|
|
410
|
+
this.privilegedTargets.set(nodeId, {
|
|
411
|
+
id: nodeId,
|
|
412
|
+
name: node.name,
|
|
413
|
+
type: node.type,
|
|
414
|
+
sid: node.sid,
|
|
415
|
+
dn: node.dn,
|
|
416
|
+
reason,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
logger.debug(`Identified ${this.privilegedTargets.size} privileged targets`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Find all attack paths using BFS
|
|
426
|
+
*/
|
|
427
|
+
private findAllAttackPaths(maxPaths: number): AttackPath[] {
|
|
428
|
+
const paths: AttackPath[] = [];
|
|
429
|
+
const targetIds = Array.from(this.privilegedTargets.keys());
|
|
430
|
+
|
|
431
|
+
// For each non-privileged node, find shortest path to any privileged target
|
|
432
|
+
for (const [sourceId, sourceNode] of this.nodes) {
|
|
433
|
+
// Skip if source is already privileged
|
|
434
|
+
if (sourceNode.isPrivileged) continue;
|
|
435
|
+
|
|
436
|
+
// Skip disabled users
|
|
437
|
+
if (sourceNode.type === 'user' && !sourceNode.isEnabled) continue;
|
|
438
|
+
|
|
439
|
+
// BFS to find shortest path to any target
|
|
440
|
+
const path = this.bfsShortestPath(sourceId, targetIds);
|
|
441
|
+
|
|
442
|
+
if (path && path.nodes.length > 1) {
|
|
443
|
+
const attackPath = this.buildAttackPath(path, sourceNode, paths.length + 1);
|
|
444
|
+
if (attackPath) {
|
|
445
|
+
paths.push(attackPath);
|
|
446
|
+
|
|
447
|
+
if (paths.length >= maxPaths) {
|
|
448
|
+
logger.debug(`Reached max paths limit: ${maxPaths}`);
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Sort by risk (critical first) then by hops (shortest first)
|
|
456
|
+
paths.sort((a, b) => {
|
|
457
|
+
const riskOrder: Record<AttackPathRisk, number> = {
|
|
458
|
+
critical: 0,
|
|
459
|
+
high: 1,
|
|
460
|
+
medium: 2,
|
|
461
|
+
low: 3,
|
|
462
|
+
};
|
|
463
|
+
if (riskOrder[a.risk] !== riskOrder[b.risk]) {
|
|
464
|
+
return riskOrder[a.risk] - riskOrder[b.risk];
|
|
465
|
+
}
|
|
466
|
+
return a.hops - b.hops;
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
return paths;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* BFS to find shortest path from source to any target
|
|
474
|
+
*/
|
|
475
|
+
private bfsShortestPath(sourceId: string, targetIds: string[]): BFSPath | null {
|
|
476
|
+
const targetSet = new Set(targetIds);
|
|
477
|
+
const visited = new Set<string>([sourceId]);
|
|
478
|
+
const queue: { nodeId: string; path: BFSPath }[] = [
|
|
479
|
+
{ nodeId: sourceId, path: { nodes: [sourceId], edges: [] } },
|
|
480
|
+
];
|
|
481
|
+
|
|
482
|
+
while (queue.length > 0) {
|
|
483
|
+
const current = queue.shift()!;
|
|
484
|
+
|
|
485
|
+
// Check if we reached a target
|
|
486
|
+
if (targetSet.has(current.nodeId) && current.path.nodes.length > 1) {
|
|
487
|
+
return current.path;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Early termination for very long paths
|
|
491
|
+
if (current.path.nodes.length > 6) continue;
|
|
492
|
+
|
|
493
|
+
// Explore neighbors
|
|
494
|
+
const outEdges = this.edges.get(current.nodeId) || [];
|
|
495
|
+
for (const edge of outEdges) {
|
|
496
|
+
if (visited.has(edge.target)) continue;
|
|
497
|
+
visited.add(edge.target);
|
|
498
|
+
|
|
499
|
+
const newPath: BFSPath = {
|
|
500
|
+
nodes: [...current.path.nodes, edge.target],
|
|
501
|
+
edges: [...current.path.edges, edge],
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
// Prioritize paths to targets
|
|
505
|
+
if (targetSet.has(edge.target)) {
|
|
506
|
+
return newPath;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
queue.push({ nodeId: edge.target, path: newPath });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Build an AttackPath from BFS result
|
|
518
|
+
*/
|
|
519
|
+
private buildAttackPath(
|
|
520
|
+
bfsPath: BFSPath,
|
|
521
|
+
sourceNode: InternalNode,
|
|
522
|
+
pathIndex: number
|
|
523
|
+
): AttackPath | null {
|
|
524
|
+
const chain: AttackChainElement[] = [];
|
|
525
|
+
|
|
526
|
+
// Build alternating node-relation-node chain
|
|
527
|
+
for (let i = 0; i < bfsPath.nodes.length; i++) {
|
|
528
|
+
const nodeId = bfsPath.nodes[i];
|
|
529
|
+
if (!nodeId) continue;
|
|
530
|
+
const node = this.nodes.get(nodeId);
|
|
531
|
+
if (!node) continue;
|
|
532
|
+
|
|
533
|
+
// Add node
|
|
534
|
+
chain.push(this.toAttackGraphNode(node));
|
|
535
|
+
|
|
536
|
+
// Add relation (if not last node)
|
|
537
|
+
if (i < bfsPath.edges.length) {
|
|
538
|
+
const edge = bfsPath.edges[i];
|
|
539
|
+
if (edge) {
|
|
540
|
+
chain.push(this.toAttackGraphRelation(edge));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Get target node
|
|
546
|
+
const targetId = bfsPath.nodes[bfsPath.nodes.length - 1];
|
|
547
|
+
if (!targetId) return null;
|
|
548
|
+
const targetNode = this.nodes.get(targetId);
|
|
549
|
+
if (!targetNode) return null;
|
|
550
|
+
|
|
551
|
+
// Classify path type
|
|
552
|
+
const pathType = this.classifyPathType(bfsPath);
|
|
553
|
+
|
|
554
|
+
// Calculate risk
|
|
555
|
+
const risk = this.calculatePathRisk(bfsPath, pathType);
|
|
556
|
+
|
|
557
|
+
// Generate description and mitigation
|
|
558
|
+
const description = this.generateDescription(bfsPath, pathType, sourceNode, targetNode);
|
|
559
|
+
const mitigation = this.generateMitigation(bfsPath, pathType);
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
id: `path-${String(pathIndex).padStart(3, '0')}`,
|
|
563
|
+
risk,
|
|
564
|
+
type: pathType,
|
|
565
|
+
hops: bfsPath.edges.length,
|
|
566
|
+
description,
|
|
567
|
+
chain,
|
|
568
|
+
entryPoint: this.toEntryPoint(sourceNode),
|
|
569
|
+
target: this.toAttackGraphNode(targetNode),
|
|
570
|
+
mitigation,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Classify path type based on edges
|
|
576
|
+
*/
|
|
577
|
+
private classifyPathType(path: BFSPath): AttackPathType {
|
|
578
|
+
const relations = path.edges.map((e) => e.relation);
|
|
579
|
+
|
|
580
|
+
// Check for DCSync
|
|
581
|
+
if (relations.includes('DCSync')) {
|
|
582
|
+
return 'DCSYNC';
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Check for delegation abuse
|
|
586
|
+
if (relations.includes('AllowedToDelegate') || relations.includes('AllowedToAct')) {
|
|
587
|
+
return 'DELEGATION_ABUSE';
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Check for Kerberoasting (user with SPN -> privileged)
|
|
591
|
+
const firstNodeId = path.nodes[0];
|
|
592
|
+
const sourceNode = firstNodeId ? this.nodes.get(firstNodeId) : undefined;
|
|
593
|
+
if (
|
|
594
|
+
sourceNode?.type === 'user' &&
|
|
595
|
+
sourceNode.servicePrincipalName &&
|
|
596
|
+
sourceNode.servicePrincipalName.length > 0
|
|
597
|
+
) {
|
|
598
|
+
return 'KERBEROASTING';
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Check for AS-REP roasting
|
|
602
|
+
if (
|
|
603
|
+
sourceNode?.userAccountControl &&
|
|
604
|
+
(sourceNode.userAccountControl & 0x400000) !== 0
|
|
605
|
+
) {
|
|
606
|
+
return 'ASREP_ROASTING';
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Check for ownership abuse
|
|
610
|
+
if (relations.includes('Owns')) {
|
|
611
|
+
return 'OWNERSHIP_ABUSE';
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Check for ACL abuse
|
|
615
|
+
const aclRelations: AttackRelationType[] = [
|
|
616
|
+
'GenericAll',
|
|
617
|
+
'WriteDacl',
|
|
618
|
+
'WriteOwner',
|
|
619
|
+
'GenericWrite',
|
|
620
|
+
'ForceChangePassword',
|
|
621
|
+
'AddMember',
|
|
622
|
+
];
|
|
623
|
+
if (relations.some((r) => aclRelations.includes(r))) {
|
|
624
|
+
return 'ACL_ABUSE';
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Check for pure group membership
|
|
628
|
+
if (relations.every((r) => r === 'MemberOf')) {
|
|
629
|
+
return 'GROUP_MEMBERSHIP';
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Default to ACL abuse
|
|
633
|
+
return 'ACL_ABUSE';
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Calculate risk level for a path
|
|
638
|
+
*/
|
|
639
|
+
private calculatePathRisk(path: BFSPath, pathType: AttackPathType): AttackPathRisk {
|
|
640
|
+
const hops = path.edges.length;
|
|
641
|
+
const hasAbusableEdge = path.edges.some((e) => e.isAbusable);
|
|
642
|
+
|
|
643
|
+
// DCSync is always critical
|
|
644
|
+
if (pathType === 'DCSYNC') return 'critical';
|
|
645
|
+
|
|
646
|
+
// Short paths with abusable edges are critical
|
|
647
|
+
if (hops <= 2 && hasAbusableEdge) return 'critical';
|
|
648
|
+
|
|
649
|
+
// Delegation abuse is high risk
|
|
650
|
+
if (pathType === 'DELEGATION_ABUSE') return 'high';
|
|
651
|
+
|
|
652
|
+
// Short paths are high risk
|
|
653
|
+
if (hops <= 2) return 'high';
|
|
654
|
+
|
|
655
|
+
// Medium paths
|
|
656
|
+
if (hops <= 4) return 'medium';
|
|
657
|
+
|
|
658
|
+
// Long paths are lower risk
|
|
659
|
+
return 'low';
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Generate path description
|
|
664
|
+
*/
|
|
665
|
+
private generateDescription(
|
|
666
|
+
path: BFSPath,
|
|
667
|
+
pathType: AttackPathType,
|
|
668
|
+
source: InternalNode,
|
|
669
|
+
target: InternalNode
|
|
670
|
+
): string {
|
|
671
|
+
const typeDescriptions: Record<AttackPathType, string> = {
|
|
672
|
+
ACL_ABUSE: 'can abuse ACL permissions to reach',
|
|
673
|
+
KERBEROASTING: 'has an SPN and can be Kerberoasted to reach',
|
|
674
|
+
ASREP_ROASTING: 'has no pre-authentication and can be AS-REP roasted to reach',
|
|
675
|
+
DELEGATION_ABUSE: 'can abuse delegation to reach',
|
|
676
|
+
LATERAL_MOVEMENT: 'can move laterally to reach',
|
|
677
|
+
CERTIFICATE_ABUSE: 'can abuse certificate services to reach',
|
|
678
|
+
GROUP_MEMBERSHIP: 'is a member of groups leading to',
|
|
679
|
+
DCSYNC: 'has DCSync rights to',
|
|
680
|
+
OWNERSHIP_ABUSE: 'owns objects leading to',
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
return `${source.name} ${typeDescriptions[pathType]} ${target.name} in ${path.edges.length} hop(s)`;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Generate mitigation advice
|
|
688
|
+
*/
|
|
689
|
+
private generateMitigation(_path: BFSPath, pathType: AttackPathType): string {
|
|
690
|
+
const mitigations: Record<AttackPathType, string> = {
|
|
691
|
+
ACL_ABUSE:
|
|
692
|
+
'Review and remove unnecessary ACL permissions. Apply least privilege principle.',
|
|
693
|
+
KERBEROASTING:
|
|
694
|
+
'Remove unnecessary SPNs from user accounts. Use Group Managed Service Accounts (gMSA) or strong passwords for service accounts.',
|
|
695
|
+
ASREP_ROASTING:
|
|
696
|
+
'Enable Kerberos pre-authentication for all user accounts.',
|
|
697
|
+
DELEGATION_ABUSE:
|
|
698
|
+
'Review and restrict delegation settings. Use resource-based constrained delegation with explicit trusts.',
|
|
699
|
+
LATERAL_MOVEMENT:
|
|
700
|
+
'Segment networks and restrict local admin access. Implement LAPS for local administrator passwords.',
|
|
701
|
+
CERTIFICATE_ABUSE:
|
|
702
|
+
'Review certificate template permissions. Disable vulnerable enrollment patterns.',
|
|
703
|
+
GROUP_MEMBERSHIP:
|
|
704
|
+
'Review nested group memberships. Remove unnecessary members from privileged groups.',
|
|
705
|
+
DCSYNC:
|
|
706
|
+
'Remove DCSync rights from non-DC accounts. Monitor for DCSync activity.',
|
|
707
|
+
OWNERSHIP_ABUSE:
|
|
708
|
+
'Review object ownership. Ensure privileged objects are owned by appropriate administrators.',
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
return mitigations[pathType];
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Convert internal node to export format
|
|
716
|
+
*/
|
|
717
|
+
private toAttackGraphNode(node: InternalNode): AttackGraphNode {
|
|
718
|
+
return {
|
|
719
|
+
id: node.id,
|
|
720
|
+
name: node.name,
|
|
721
|
+
type: node.type,
|
|
722
|
+
sid: node.sid,
|
|
723
|
+
dn: node.dn,
|
|
724
|
+
domain: this.domainName,
|
|
725
|
+
isEnabled: node.isEnabled,
|
|
726
|
+
isPrivileged: node.isPrivileged,
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Convert internal edge to export relation
|
|
732
|
+
*/
|
|
733
|
+
private toAttackGraphRelation(edge: InternalEdge): AttackGraphRelation {
|
|
734
|
+
return {
|
|
735
|
+
relation: edge.relation,
|
|
736
|
+
isAbusable: edge.isAbusable,
|
|
737
|
+
accessMask: edge.accessMask,
|
|
738
|
+
objectType: edge.objectType,
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Convert node to entry point
|
|
744
|
+
*/
|
|
745
|
+
private toEntryPoint(node: InternalNode): AttackEntryPoint {
|
|
746
|
+
return {
|
|
747
|
+
id: node.id,
|
|
748
|
+
name: node.name,
|
|
749
|
+
type: node.type,
|
|
750
|
+
properties: {
|
|
751
|
+
hasSPN: node.servicePrincipalName && node.servicePrincipalName.length > 0,
|
|
752
|
+
noPreauth: node.userAccountControl
|
|
753
|
+
? (node.userAccountControl & 0x400000) !== 0
|
|
754
|
+
: false,
|
|
755
|
+
passwordNotExpire: node.userAccountControl
|
|
756
|
+
? (node.userAccountControl & 0x10000) !== 0
|
|
757
|
+
: false,
|
|
758
|
+
unconstrained: node.userAccountControl
|
|
759
|
+
? (node.userAccountControl & 0x80000) !== 0
|
|
760
|
+
: false,
|
|
761
|
+
constrained: node.delegateTo && node.delegateTo.length > 0,
|
|
762
|
+
rbcd: node.rbcdFrom && node.rbcdFrom.length > 0,
|
|
763
|
+
adminCount: node.adminCount === 1,
|
|
764
|
+
enabled: node.isEnabled,
|
|
765
|
+
},
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Compute statistics
|
|
771
|
+
*/
|
|
772
|
+
private computeStats(paths: AttackPath[]): AttackGraphStats {
|
|
773
|
+
const byRisk = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
774
|
+
const byType: Record<AttackPathType, number> = {
|
|
775
|
+
ACL_ABUSE: 0,
|
|
776
|
+
KERBEROASTING: 0,
|
|
777
|
+
ASREP_ROASTING: 0,
|
|
778
|
+
DELEGATION_ABUSE: 0,
|
|
779
|
+
LATERAL_MOVEMENT: 0,
|
|
780
|
+
CERTIFICATE_ABUSE: 0,
|
|
781
|
+
GROUP_MEMBERSHIP: 0,
|
|
782
|
+
DCSYNC: 0,
|
|
783
|
+
OWNERSHIP_ABUSE: 0,
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
let totalHops = 0;
|
|
787
|
+
let shortestPath = Infinity;
|
|
788
|
+
let longestPath = 0;
|
|
789
|
+
|
|
790
|
+
for (const path of paths) {
|
|
791
|
+
byRisk[path.risk]++;
|
|
792
|
+
byType[path.type]++;
|
|
793
|
+
totalHops += path.hops;
|
|
794
|
+
shortestPath = Math.min(shortestPath, path.hops);
|
|
795
|
+
longestPath = Math.max(longestPath, path.hops);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return {
|
|
799
|
+
totalPaths: paths.length,
|
|
800
|
+
byRisk,
|
|
801
|
+
byType,
|
|
802
|
+
averageHops: paths.length > 0 ? Math.round((totalHops / paths.length) * 100) / 100 : 0,
|
|
803
|
+
shortestPath: paths.length > 0 ? shortestPath : 0,
|
|
804
|
+
longestPath: paths.length > 0 ? longestPath : 0,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Get unique nodes involved in paths
|
|
810
|
+
*/
|
|
811
|
+
private getUniqueNodes(paths: AttackPath[]): AttackGraphUniqueNode[] {
|
|
812
|
+
const nodeCount = new Map<string, { node: AttackGraphNode; count: number }>();
|
|
813
|
+
|
|
814
|
+
for (const path of paths) {
|
|
815
|
+
for (const element of path.chain) {
|
|
816
|
+
if (isAttackGraphNode(element)) {
|
|
817
|
+
const existing = nodeCount.get(element.id);
|
|
818
|
+
if (existing) {
|
|
819
|
+
existing.count++;
|
|
820
|
+
} else {
|
|
821
|
+
nodeCount.set(element.id, { node: element, count: 1 });
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return Array.from(nodeCount.values())
|
|
828
|
+
.map(({ node, count }) => ({
|
|
829
|
+
id: node.id,
|
|
830
|
+
name: node.name,
|
|
831
|
+
type: node.type,
|
|
832
|
+
pathCount: count,
|
|
833
|
+
sid: node.sid,
|
|
834
|
+
}))
|
|
835
|
+
.sort((a, b) => b.pathCount - a.pathCount);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// ========== Helper Methods ==========
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Add an edge to the graph
|
|
842
|
+
*/
|
|
843
|
+
private addEdge(edge: InternalEdge): void {
|
|
844
|
+
const sourceEdges = this.edges.get(edge.source);
|
|
845
|
+
if (sourceEdges) {
|
|
846
|
+
sourceEdges.push(edge);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const targetEdges = this.reverseEdges.get(edge.target);
|
|
850
|
+
if (targetEdges) {
|
|
851
|
+
targetEdges.push(edge);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Count total edges
|
|
857
|
+
*/
|
|
858
|
+
private countEdges(): number {
|
|
859
|
+
let count = 0;
|
|
860
|
+
for (const edges of this.edges.values()) {
|
|
861
|
+
count += edges.length;
|
|
862
|
+
}
|
|
863
|
+
return count;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Extract SID from AD object
|
|
868
|
+
*/
|
|
869
|
+
private extractSid(obj: ADUser | ADGroup | ADComputer): string | undefined {
|
|
870
|
+
const sid = (obj as any).objectSid || (obj as any).objectSID || (obj as any).sid;
|
|
871
|
+
if (typeof sid === 'string') return sid;
|
|
872
|
+
if (Buffer.isBuffer(sid)) return this.bufferToSid(sid);
|
|
873
|
+
return undefined;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Convert Buffer to SID string
|
|
878
|
+
*/
|
|
879
|
+
private bufferToSid(buffer: Buffer): string {
|
|
880
|
+
if (buffer.length < 8) return '';
|
|
881
|
+
|
|
882
|
+
const revision = buffer.readUInt8(0);
|
|
883
|
+
const subAuthCount = buffer.readUInt8(1);
|
|
884
|
+
const authority =
|
|
885
|
+
buffer.readUInt8(2) * Math.pow(2, 40) +
|
|
886
|
+
buffer.readUInt8(3) * Math.pow(2, 32) +
|
|
887
|
+
buffer.readUInt8(4) * Math.pow(2, 24) +
|
|
888
|
+
buffer.readUInt8(5) * Math.pow(2, 16) +
|
|
889
|
+
buffer.readUInt8(6) * Math.pow(2, 8) +
|
|
890
|
+
buffer.readUInt8(7);
|
|
891
|
+
|
|
892
|
+
let sid = `S-${revision}-${authority}`;
|
|
893
|
+
|
|
894
|
+
for (let i = 0; i < subAuthCount; i++) {
|
|
895
|
+
const offset = 8 + i * 4;
|
|
896
|
+
if (offset + 4 <= buffer.length) {
|
|
897
|
+
const subAuth = buffer.readUInt32LE(offset);
|
|
898
|
+
sid += `-${subAuth}`;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
return sid;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Extract domain SID from first user/computer SID
|
|
907
|
+
*/
|
|
908
|
+
private extractDomainSid(): string {
|
|
909
|
+
for (const user of this.users) {
|
|
910
|
+
const sid = this.extractSid(user);
|
|
911
|
+
if (sid) {
|
|
912
|
+
// Domain SID is everything except the last RID
|
|
913
|
+
const parts = sid.split('-');
|
|
914
|
+
if (parts.length > 4) {
|
|
915
|
+
return parts.slice(0, -1).join('-');
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return '';
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* Check if SID is privileged
|
|
924
|
+
*/
|
|
925
|
+
private isPrivilegedBySid(sid?: string): boolean {
|
|
926
|
+
if (!sid) return false;
|
|
927
|
+
|
|
928
|
+
for (const suffix of Object.values(PRIVILEGED_SID_SUFFIXES)) {
|
|
929
|
+
if (sid.endsWith(suffix)) return true;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
return false;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Generate ID from DN
|
|
937
|
+
*/
|
|
938
|
+
private generateId(dn: string): string {
|
|
939
|
+
return `dn:${dn.toLowerCase()}`;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Get SPN array from user
|
|
944
|
+
*/
|
|
945
|
+
private getSpn(user: ADUser): string[] | undefined {
|
|
946
|
+
const spn = (user as any).servicePrincipalName;
|
|
947
|
+
if (!spn) return undefined;
|
|
948
|
+
return Array.isArray(spn) ? spn : [spn];
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Get delegation targets
|
|
953
|
+
*/
|
|
954
|
+
private getDelegationTargets(obj: ADUser | ADComputer): string[] | undefined {
|
|
955
|
+
const attr = (obj as any)['msDS-AllowedToDelegateTo'];
|
|
956
|
+
if (!attr) return undefined;
|
|
957
|
+
return Array.isArray(attr) ? attr : [attr];
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Get RBCD principals
|
|
962
|
+
*/
|
|
963
|
+
private getRbcdPrincipals(computer: ADComputer): string[] | undefined {
|
|
964
|
+
const attr = (computer as any)['msDS-AllowedToActOnBehalfOfOtherIdentity'];
|
|
965
|
+
if (!attr) return undefined;
|
|
966
|
+
|
|
967
|
+
// This is typically a binary security descriptor - would need to parse it
|
|
968
|
+
// For now, just track that RBCD is configured
|
|
969
|
+
return [];
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Resolve ACL trustee to node ID
|
|
974
|
+
*/
|
|
975
|
+
private resolveAclTrustee(trustee: string): string | undefined {
|
|
976
|
+
// Trustee might be a SID or DN
|
|
977
|
+
if (trustee.startsWith('S-1-')) {
|
|
978
|
+
// It's a SID - look up by SID
|
|
979
|
+
if (this.nodes.has(trustee)) {
|
|
980
|
+
return trustee;
|
|
981
|
+
}
|
|
982
|
+
return this.findNodeBySid(trustee);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// It's a DN
|
|
986
|
+
return this.dnToId.get(trustee.toLowerCase());
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* Find node by SID
|
|
991
|
+
*/
|
|
992
|
+
private findNodeBySid(sid: string): string | undefined {
|
|
993
|
+
for (const [nodeId, node] of this.nodes) {
|
|
994
|
+
if (node.sid === sid) return nodeId;
|
|
995
|
+
}
|
|
996
|
+
return undefined;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Resolve SPN to target node
|
|
1001
|
+
*/
|
|
1002
|
+
private resolveSPNTarget(spn: string): string | undefined {
|
|
1003
|
+
// SPN format: service/host:port or service/host
|
|
1004
|
+
const parts = spn.split('/');
|
|
1005
|
+
if (parts.length < 2 || !parts[1]) return undefined;
|
|
1006
|
+
|
|
1007
|
+
const hostPart = parts[1].split(':')[0];
|
|
1008
|
+
if (!hostPart) return undefined;
|
|
1009
|
+
const host = hostPart.toLowerCase();
|
|
1010
|
+
|
|
1011
|
+
// Look for computer with matching hostname
|
|
1012
|
+
const hostShort = host.split('.')[0] || host;
|
|
1013
|
+
for (const [nodeId, node] of this.nodes) {
|
|
1014
|
+
if (node.type === 'computer') {
|
|
1015
|
+
const nodeName = node.name.toLowerCase().replace(/\$$/, '');
|
|
1016
|
+
if (nodeName === host || nodeName.startsWith(hostShort)) {
|
|
1017
|
+
return nodeId;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
return undefined;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Classify ACL to relation types
|
|
1027
|
+
*/
|
|
1028
|
+
private classifyAclRelation(
|
|
1029
|
+
acl: AclEntry
|
|
1030
|
+
): { type: AttackRelationType; isAbusable: boolean }[] {
|
|
1031
|
+
const relations: { type: AttackRelationType; isAbusable: boolean }[] = [];
|
|
1032
|
+
const mask = acl.accessMask;
|
|
1033
|
+
const objectType = acl.objectType?.toLowerCase();
|
|
1034
|
+
|
|
1035
|
+
// GenericAll
|
|
1036
|
+
if (mask & ACCESS_MASK.GENERIC_ALL) {
|
|
1037
|
+
relations.push({ type: 'GenericAll', isAbusable: true });
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// GenericWrite
|
|
1041
|
+
if (mask & ACCESS_MASK.GENERIC_WRITE) {
|
|
1042
|
+
relations.push({ type: 'GenericWrite', isAbusable: true });
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// WriteDacl
|
|
1046
|
+
if (mask & ACCESS_MASK.WRITE_DACL) {
|
|
1047
|
+
relations.push({ type: 'WriteDacl', isAbusable: true });
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// WriteOwner
|
|
1051
|
+
if (mask & ACCESS_MASK.WRITE_OWNER) {
|
|
1052
|
+
relations.push({ type: 'WriteOwner', isAbusable: true });
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// Extended rights - check object type
|
|
1056
|
+
if (mask & ACCESS_MASK.CONTROL_ACCESS && objectType) {
|
|
1057
|
+
// ForceChangePassword
|
|
1058
|
+
if (objectType === ACL_GUIDS.FORCE_CHANGE_PASSWORD.toLowerCase()) {
|
|
1059
|
+
relations.push({ type: 'ForceChangePassword', isAbusable: true });
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// DCSync rights
|
|
1063
|
+
if (
|
|
1064
|
+
objectType === ACL_GUIDS.DS_REPLICATION_GET_CHANGES.toLowerCase() ||
|
|
1065
|
+
objectType === ACL_GUIDS.DS_REPLICATION_GET_CHANGES_ALL.toLowerCase()
|
|
1066
|
+
) {
|
|
1067
|
+
relations.push({ type: 'DCSync', isAbusable: true });
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// Self membership (AddMember)
|
|
1072
|
+
if (mask & ACCESS_MASK.SELF && objectType === ACL_GUIDS.SELF_MEMBERSHIP.toLowerCase()) {
|
|
1073
|
+
relations.push({ type: 'AddMember', isAbusable: true });
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
return relations;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* Create and export attack graph
|
|
1082
|
+
*/
|
|
1083
|
+
export function computeAttackGraph(
|
|
1084
|
+
users: ADUser[],
|
|
1085
|
+
groups: ADGroup[],
|
|
1086
|
+
computers: ADComputer[],
|
|
1087
|
+
aclEntries: AclEntry[],
|
|
1088
|
+
certTemplates: ADCSCertificateTemplate[] = [],
|
|
1089
|
+
gpos: ADGPO[] = [],
|
|
1090
|
+
domain?: { name?: string; sid?: string },
|
|
1091
|
+
maxPaths: number = 500
|
|
1092
|
+
): AttackGraphExport {
|
|
1093
|
+
const service = new AttackGraphService(
|
|
1094
|
+
users,
|
|
1095
|
+
groups,
|
|
1096
|
+
computers,
|
|
1097
|
+
aclEntries,
|
|
1098
|
+
certTemplates,
|
|
1099
|
+
gpos,
|
|
1100
|
+
domain
|
|
1101
|
+
);
|
|
1102
|
+
|
|
1103
|
+
return service.export(maxPaths);
|
|
1104
|
+
}
|