using Bit.Api.Dirt.Models; using Bit.Api.Dirt.Models.Response; using Bit.Api.Tools.Models.Response; using Bit.Core.Context; using Bit.Core.Dirt.Entities; using Bit.Core.Dirt.Reports.Models.Data; using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; using Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces; using Bit.Core.Dirt.Reports.ReportFeatures.Requests; using Bit.Core.Exceptions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Bit.Api.Dirt.Controllers; [Route("reports")] [Authorize("Application")] public class ReportsController : Controller { private readonly ICurrentContext _currentContext; private readonly IMemberAccessReportQuery _memberAccessReportQuery; private readonly IRiskInsightsReportQuery _riskInsightsReportQuery; private readonly IAddPasswordHealthReportApplicationCommand _addPwdHealthReportAppCommand; private readonly IGetPasswordHealthReportApplicationQuery _getPwdHealthReportAppQuery; private readonly IDropPasswordHealthReportApplicationCommand _dropPwdHealthReportAppCommand; private readonly IAddOrganizationReportCommand _addOrganizationReportCommand; private readonly IDropOrganizationReportCommand _dropOrganizationReportCommand; private readonly IGetOrganizationReportQuery _getOrganizationReportQuery; public ReportsController( ICurrentContext currentContext, IMemberAccessReportQuery memberAccessReportQuery, IRiskInsightsReportQuery riskInsightsReportQuery, IAddPasswordHealthReportApplicationCommand addPasswordHealthReportApplicationCommand, IGetPasswordHealthReportApplicationQuery getPasswordHealthReportApplicationQuery, IDropPasswordHealthReportApplicationCommand dropPwdHealthReportAppCommand, IGetOrganizationReportQuery getOrganizationReportQuery, IAddOrganizationReportCommand addOrganizationReportCommand, IDropOrganizationReportCommand dropOrganizationReportCommand ) { _currentContext = currentContext; _memberAccessReportQuery = memberAccessReportQuery; _riskInsightsReportQuery = riskInsightsReportQuery; _addPwdHealthReportAppCommand = addPasswordHealthReportApplicationCommand; _getPwdHealthReportAppQuery = getPasswordHealthReportApplicationQuery; _dropPwdHealthReportAppCommand = dropPwdHealthReportAppCommand; _getOrganizationReportQuery = getOrganizationReportQuery; _addOrganizationReportCommand = addOrganizationReportCommand; _dropOrganizationReportCommand = dropOrganizationReportCommand; } /// /// Organization member information containing a list of cipher ids /// assigned /// /// Organzation Id /// IEnumerable of MemberCipherDetailsResponseModel /// If Access reports permission is not assigned [HttpGet("member-cipher-details/{orgId}")] public async Task> GetMemberCipherDetails(Guid orgId) { // Using the AccessReports permission here until new permissions // are needed for more control over reports if (!await _currentContext.AccessReports(orgId)) { throw new NotFoundException(); } var riskDetails = await GetRiskInsightsReportDetails(new RiskInsightsReportRequest { OrganizationId = orgId }); var responses = riskDetails.Select(x => new MemberCipherDetailsResponseModel(x)); return responses; } /// /// Access details for an organization member. Includes the member information, /// group collection assignment, and item counts /// /// Organization Id /// IEnumerable of MemberAccessReportResponseModel /// If Access reports permission is not assigned [HttpGet("member-access/{orgId}")] public async Task> GetMemberAccessReport(Guid orgId) { if (!await _currentContext.AccessReports(orgId)) { throw new NotFoundException(); } var accessDetails = await GetMemberAccessDetails(new MemberAccessReportRequest { OrganizationId = orgId }); var responses = accessDetails.Select(x => new MemberAccessDetailReportResponseModel(x)); return responses; } /// /// Contains the organization member info, the cipher ids associated with the member, /// and details on their collections, groups, and permissions /// /// Request parameters /// /// List of a user's permissions at a group and collection level as well as the number of ciphers /// associated with that group/collection /// private async Task> GetMemberAccessDetails( MemberAccessReportRequest request) { var accessDetails = await _memberAccessReportQuery.GetMemberAccessReportsAsync(request); return accessDetails; } /// /// Gets the risk insights report details from the risk insights query. Associates a user to their cipher ids /// /// Request parameters /// A list of risk insights data associating the user to cipher ids private async Task> GetRiskInsightsReportDetails( RiskInsightsReportRequest request) { var riskDetails = await _riskInsightsReportQuery.GetRiskInsightsReportDetails(request); return riskDetails; } /// /// Get the password health report applications for an organization /// /// A valid Organization Id /// An Enumerable of PasswordHealthReportApplication /// If the user lacks access /// If the organization Id is not valid [HttpGet("password-health-report-applications/{orgId}")] public async Task> GetPasswordHealthReportApplications(Guid orgId) { if (!await _currentContext.AccessReports(orgId)) { throw new NotFoundException(); } return await _getPwdHealthReportAppQuery.GetPasswordHealthReportApplicationAsync(orgId); } /// /// Adds a new record into PasswordHealthReportApplication /// /// A single instance of PasswordHealthReportApplication Model /// A single instance of PasswordHealthReportApplication /// If the organization Id is not valid /// If the user lacks access [HttpPost("password-health-report-application")] public async Task AddPasswordHealthReportApplication( [FromBody] PasswordHealthReportApplicationModel request) { if (!await _currentContext.AccessReports(request.OrganizationId)) { throw new NotFoundException(); } var commandRequest = new AddPasswordHealthReportApplicationRequest { OrganizationId = request.OrganizationId, Url = request.Url }; return await _addPwdHealthReportAppCommand.AddPasswordHealthReportApplicationAsync(commandRequest); } /// /// Adds multiple records into PasswordHealthReportApplication /// /// A enumerable of PasswordHealthReportApplicationModel /// An Enumerable of PasswordHealthReportApplication /// If user does not have access to the OrganizationId /// If the organization Id is not valid [HttpPost("password-health-report-applications")] public async Task> AddPasswordHealthReportApplications( [FromBody] IEnumerable request) { if (request.Any(_ => _currentContext.AccessReports(_.OrganizationId).Result == false)) { throw new NotFoundException(); } var commandRequests = request.Select(request => new AddPasswordHealthReportApplicationRequest { OrganizationId = request.OrganizationId, Url = request.Url }).ToList(); return await _addPwdHealthReportAppCommand.AddPasswordHealthReportApplicationAsync(commandRequests); } /// /// Drops a record from PasswordHealthReportApplication /// /// /// A single instance of DropPasswordHealthReportApplicationRequest /// { OrganizationId, array of PasswordHealthReportApplicationIds } /// /// /// If user does not have access to the organization /// If the organization does not have any records [HttpDelete("password-health-report-application")] public async Task DropPasswordHealthReportApplication( [FromBody] DropPasswordHealthReportApplicationRequest request) { if (!await _currentContext.AccessReports(request.OrganizationId)) { throw new NotFoundException(); } await _dropPwdHealthReportAppCommand.DropPasswordHealthReportApplicationAsync(request); } /// /// Adds a new organization report /// /// A single instance of AddOrganizationReportRequest /// A single instance of OrganizationReport /// If user does not have access to the organization /// If the organization Id is not valid [HttpPost("organization-reports")] public async Task AddOrganizationReport([FromBody] AddOrganizationReportRequest request) { if (!await _currentContext.AccessReports(request.OrganizationId)) { throw new NotFoundException(); } return await _addOrganizationReportCommand.AddOrganizationReportAsync(request); } /// /// Drops organization reports for an organization /// /// A single instance of DropOrganizationReportRequest /// /// If user does not have access to the organization /// If the organization does not have any records [HttpDelete("organization-reports")] public async Task DropOrganizationReport([FromBody] DropOrganizationReportRequest request) { if (!await _currentContext.AccessReports(request.OrganizationId)) { throw new NotFoundException(); } await _dropOrganizationReportCommand.DropOrganizationReportAsync(request); } /// /// Gets organization reports for an organization /// /// A valid Organization Id /// An Enumerable of OrganizationReport /// If user does not have access to the organization /// If the organization Id is not valid [HttpGet("organization-reports/{orgId}")] public async Task> GetOrganizationReports(Guid orgId) { if (!await _currentContext.AccessReports(orgId)) { throw new NotFoundException(); } return await _getOrganizationReportQuery.GetOrganizationReportAsync(orgId); } /// /// Gets the latest organization report for an organization /// /// A valid Organization Id /// A single instance of OrganizationReport /// If user does not have access to the organization /// If the organization Id is not valid [HttpGet("organization-reports/latest/{orgId}")] public async Task GetLatestOrganizationReport(Guid orgId) { if (!await _currentContext.AccessReports(orgId)) { throw new NotFoundException(); } return await _getOrganizationReportQuery.GetLatestOrganizationReportAsync(orgId); } /// /// Gets the Organization Report Summary for an organization. /// This includes the latest report's encrypted data, encryption key, and date. /// This is a mock implementation and should be replaced with actual data retrieval logic. /// /// /// Min date (example: 2023-01-01) /// Max date (example: 2023-12-31) /// /// [HttpGet("organization-report-summary/{orgId}")] public IEnumerable GetOrganizationReportSummary( [FromRoute] Guid orgId, [FromQuery] DateOnly from, [FromQuery] DateOnly to) { if (!ModelState.IsValid) { throw new BadRequestException(ModelState); } GuardOrganizationAccess(orgId); // FIXME: remove this mock class when actual data retrieval is implemented return MockOrganizationReportSummary.GetMockData() .Where(_ => _.OrganizationId == orgId && _.Date >= from.ToDateTime(TimeOnly.MinValue) && _.Date <= to.ToDateTime(TimeOnly.MaxValue)); } /// /// Creates a new Organization Report Summary for an organization. /// This is a mock implementation and should be replaced with actual creation logic. /// /// /// Returns 204 Created with the created OrganizationReportSummaryModel /// [HttpPost("organization-report-summary")] public IActionResult CreateOrganizationReportSummary([FromBody] OrganizationReportSummaryModel model) { if (!ModelState.IsValid) { throw new BadRequestException(ModelState); } GuardOrganizationAccess(model.OrganizationId); // TODO: Implement actual creation logic // Returns 204 No Content as a placeholder return NoContent(); } [HttpPut("organization-report-summary")] public IActionResult UpdateOrganizationReportSummary([FromBody] OrganizationReportSummaryModel model) { if (!ModelState.IsValid) { throw new BadRequestException(ModelState); } GuardOrganizationAccess(model.OrganizationId); // TODO: Implement actual update logic // Returns 204 No Content as a placeholder return NoContent(); } private void GuardOrganizationAccess(Guid organizationId) { if (!_currentContext.AccessReports(organizationId).Result) { throw new NotFoundException(); } } // FIXME: remove this mock class when actual data retrieval is implemented private class MockOrganizationReportSummary { public static List GetMockData() { return new List { new OrganizationReportSummaryModel { OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"), EncryptedData = "2.EtCcxDEBoF1MYChYHC4Q1w==|RyZ07R7qEFBbc/ICLFpEMockL9K+PD6rOod6DGHHrkaRLHUDqDwmxbu3jnD0cg8s7GIYmp0jApHXC+82QdApk87pA0Kr8fN2Rj0+8bDQCjhKfoRTipAB25S/n2E+ttjvlFfag92S66XqUH9S/eZw/Q==|0bPfykHk3SqS/biLNcNoYtH6YTstBEKu3AhvdZZLxhU=", EncryptionKey = "2.Dd/TtdNwxWdYg9+fRkxh6w==|8KAiK9SoadgFRmyVOchd4tNh2vErD1Rv9x1gqtsE5tzxKE/V/5kkr1WuVG+QpEj//YaQt221UEMESRSXicZ7a9cB6xXLBkbbFwmecQRJVBs=|902em44n9cwciZzYrYuX6MRzRa+4hh1HHfNAxyJx/IM=", Date = DateTime.UtcNow }, new OrganizationReportSummaryModel { OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"), EncryptedData = "2.HvY4fAvbzYV1hqa3255m5Q==|WcKga2Wka5i8fVso8MgjzfBAwxaqdhZDL3bnvhDsisZ0r9lNKQcG3YUQSFpJxr74cgg5QRQaFieCUe2YppciHDT6bsaE2VzFce3cNNB821uTFqnlJClkGJpG1nGvPupdErrg4Ik57WenEzYesmR4pw==|F0aJfF+1MlPm+eAlQnDgFnwfv198N9VtPqFJa4+UFqk=", EncryptionKey = "2.ctMgLN4ycPusbQArG/uiag==|NtqiQsAoUxMSTBQsxAMyVLWdt5lVEUGZQNxZSBU4l76ywH2f6dx5FWFrcF3t3GBqy5yDoc5eBg0VlJDW9coqzp8j9n8h1iMrtmXPyBMAhbc=|pbH+w68BUdUKYCfNRpjd8NENw2lZ0vfxgMuTrsrRCTQ=", Date = DateTime.UtcNow.AddMonths(-1) }, new OrganizationReportSummaryModel { OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"), EncryptedData = "2.NH4qLZYUkz/+qpB/mRsLTA==|LEFt05jJz0ngh+Hl5lqk6kebj7lZMefA3eFdL1kLJSGdD3uTOngRwH7GXLQNFeQOxutnLX9YUILbUEPwaM8gCwNQ1KWYdB1Z+Ky4nzKRb60N7L5aTA2za6zXTIdjv7Zwhg0jPZ6sPevTuvSyqjMCuA==|Uuu6gZaF0wvB2mHFwtvHegMxfe8DgsYWTRfGiVn4lkM=", EncryptionKey = "2.3YwG78ykSxAn44NcymdG4w==|4jfn0nLoFielicAFbmq27DNUUjV4SwGePnjYRmOa7hk4pEPnQRS3MsTJFbutVyXOgKFY9Yn2yGFZownY9EmXOMM+gHPD0t6TfzUKqQcRyuI=|wasP9zZEL9mFH5HzJYrMxnKUr/XlFKXCxG9uW66uaPU=", Date = DateTime.UtcNow.AddMonths(-1) }, new OrganizationReportSummaryModel { OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"), EncryptedData = "2.YmKWj/707wDPONh+JXPBOw==|Fx4jcUHmnUnSMCU8vdThMSYpDyKPnC09TxpSbNxia0M6MFbd5WHElcVribrYgTENyU0HlqPW43hThJ6xXCM0EjEWP7/jb/0l07vMNkA7sDYq+czf0XnYZgZSGKh06wFVz8xkhaPTdsiO4CXuMsoH+w==|DDVwVFHzdfbPQe3ycCx82eYVHDW97V/eWTPsNpHX/+U=", EncryptionKey = "2.f/U45I7KF+JKfnvOArUyaw==|zNhhS2q2WwBl6SqLWMkxrXC8EX91Ra9LJExywkJhsRbxubRLt7fK+YWc8T1LUaDmMwJ3G8buSPGzyacKX0lnUR33dW6DIaLNgRZ/ekb/zkg=|qFoIZWwS0foiiIOyikFRwQKmmmI2HeyHcOVklJnIILI=", Date = DateTime.UtcNow.AddMonths(-1) }, new OrganizationReportSummaryModel { OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"), EncryptedData = "2.WYauwooJUEY3kZsDPphmrA==|oguYW6h10A4GxK4KkRS0X32qSTekU2CkGqNDNGfisUgvJzsyoVTafO9sVcdPdg4BUM7YNkPMjYiKEc5jMHkIgLzbnM27jcGvMJrrccSrLHiWL6/mEiqQkV3TlfiZF9i3wqj1ITsYRzM454uNle6Wrg==|uR67aFYb1i5LSidWib0iTf8091l8GY5olHkVXse3CAw=", EncryptionKey = "2.ZyV9+9A2cxNaf8dfzfbnlA==|hhorBpVkcrrhTtNmd6SNHYI8gPNokGLOC22Vx8Qa/AotDAcyuYWw56zsawMnzpAdJGEJFtszKM2+VUVOcroCTMWHpy8yNf/kZA6uPk3Lz3s=|ASzVeJf+K1ZB8NXuypamRBGRuRq0GUHZBEy5r/O7ORY=", Date = DateTime.UtcNow.AddMonths(-1) }, }; } } }