├── Template ├── .gitignore ├── jquery.min.js.gz ├── popper.min.js.gz ├── bootstrap.min.css.gz ├── bootstrap.min.js.gz ├── bootstrap-table.min.js.gz ├── bootstrap-table.min.css.gz ├── responsivetemplate.html ├── ReportBase.js ├── ProcessTemplate.ps1 ├── TemplateManager.cs ├── ReportMain.js └── ReportBase.css ├── LICENSE.md ├── pingcastle.ico ├── packages.config ├── RESTServices ├── IAzureService.cs ├── ClientIDs.cs ├── AzureServiceAttribute.cs ├── EndPointAttribute.cs ├── Azure │ ├── AzureADConnectApi.cs │ ├── MicrosoftGraph.cs │ └── ManagementApi.cs ├── O365 │ └── O365Api.cs └── RESTClientBase.cs ├── PingCastleCloudException.cs ├── Tokens ├── JwtHeader.cs ├── ChallengeResponse.cs ├── JwtPayload.cs ├── TokenCache.cs ├── Token.cs ├── JwtToken.cs └── CookieManager.cs ├── App.config ├── Credentials ├── UserCredential.cs ├── PRTCredential.cs ├── IAzureCredential.cs ├── CredentialBase.cs └── CertificateCredential.cs ├── PingCastleCloud.sln ├── Rules ├── UserConsentCompanyData.cs ├── UserRegisterApplications.cs ├── GuestUserAccessRestriction2.cs ├── GuestUserAccessRestriction1.cs ├── ADConnectVersion1.cs ├── ADConnectVersion.cs ├── CustomRulesSettings.cs ├── RuleBase.cs ├── RuleSet.cs └── RuleDescription.Designer.cs ├── Logs ├── LoggingHandler.cs └── SazGenerator.cs ├── PublicServices ├── UserRealmCTRequest.cs ├── UserRealmV1.cs ├── UserRealmV2.cs ├── UserRealmSRF.cs ├── UserRealmCT.cs ├── OpenIDConfiguration.cs ├── PublicService.cs └── TenantBrandingInfo.cs ├── Constants.cs ├── Properties └── AssemblyInfo.cs ├── CreateSecureAppCert.ps1 ├── README.md ├── UI └── AuthenticationDialog.Designer.cs ├── Common ├── JsonSerialization.cs └── HttpClientHelper.cs ├── .gitignore ├── PingCastleCloud.csproj ├── Export └── ExportAsGuest.cs ├── License.cs └── Data └── HealthCheckCloudData.cs /Template/.gitignore: -------------------------------------------------------------------------------- 1 | *.min.css 2 | *.min.js 3 | *.gz 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netwrix/PingCastleCloud/HEAD/LICENSE.md -------------------------------------------------------------------------------- /pingcastle.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netwrix/PingCastleCloud/HEAD/pingcastle.ico -------------------------------------------------------------------------------- /Template/jquery.min.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netwrix/PingCastleCloud/HEAD/Template/jquery.min.js.gz -------------------------------------------------------------------------------- /Template/popper.min.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netwrix/PingCastleCloud/HEAD/Template/popper.min.js.gz -------------------------------------------------------------------------------- /Template/bootstrap.min.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netwrix/PingCastleCloud/HEAD/Template/bootstrap.min.css.gz -------------------------------------------------------------------------------- /Template/bootstrap.min.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netwrix/PingCastleCloud/HEAD/Template/bootstrap.min.js.gz -------------------------------------------------------------------------------- /Template/bootstrap-table.min.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netwrix/PingCastleCloud/HEAD/Template/bootstrap-table.min.js.gz -------------------------------------------------------------------------------- /Template/bootstrap-table.min.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netwrix/PingCastleCloud/HEAD/Template/bootstrap-table.min.css.gz -------------------------------------------------------------------------------- /packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /RESTServices/IAzureService.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | 13 | namespace PingCastleCloud.RESTServices.Azure 14 | { 15 | public interface IAzureService 16 | { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Template/responsivetemplate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | <%=Header%> 12 | 13 | 14 | 15 | 16 | <%=Body%> 17 | 18 | <%=Footer%> 19 | 20 | 21 | -------------------------------------------------------------------------------- /PingCastleCloudException.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Text; 11 | 12 | namespace PingCastleCloud 13 | { 14 | class PingCastleCloudException : Exception 15 | { 16 | public PingCastleCloudException(string message) 17 | : base(message) 18 | { 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /RESTServices/ClientIDs.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | 13 | namespace PingCastleCloud.RESTServices.Azure 14 | { 15 | public class ClientIDs 16 | { 17 | public static readonly Guid GraphAPI = new Guid("1b730954-1685-4b74-9bfd-dac224a7b894"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tokens/JwtHeader.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.Common; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Text; 12 | using System.Threading.Tasks; 13 | 14 | namespace PingCastleCloud.Tokens 15 | { 16 | public class JwtHeader : JsonSerialization 17 | { 18 | public string alg { get; set; } 19 | public string typ { get; set; } 20 | public string x5t { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tokens/ChallengeResponse.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.Common; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Runtime.Serialization; 12 | using System.Text; 13 | using System.Threading.Tasks; 14 | 15 | namespace PingCastleCloud.RESTServices 16 | { 17 | [DataContractAttribute] 18 | public class ChallengeResponse : JsonSerialization 19 | { 20 | [DataMember] 21 | public string Nonce { get; set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Credentials/UserCredential.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.RESTServices.Azure; 8 | using PingCastleCloud.Tokens; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Linq; 12 | using System.Text; 13 | using System.Threading.Tasks; 14 | 15 | namespace PingCastleCloud.Credentials 16 | { 17 | public class UserCredential : CredentialBase 18 | { 19 | public UserCredential() : base() 20 | { 21 | } 22 | 23 | public UserCredential(string tenantid) : base(tenantid) 24 | { 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Credentials/PRTCredential.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.RESTServices.Azure; 8 | using PingCastleCloud.Tokens; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Linq; 12 | using System.Text; 13 | using System.Threading.Tasks; 14 | 15 | namespace PingCastleCloud.Credentials 16 | { 17 | 18 | public class PRTCredential : CredentialBase 19 | { 20 | public PRTCredential() : base() 21 | { 22 | } 23 | 24 | public PRTCredential(string tenantid) : base(tenantid) 25 | { 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Credentials/IAzureCredential.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.RESTServices.Azure; 8 | using PingCastleCloud.Tokens; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Linq; 12 | using System.Text; 13 | using System.Threading.Tasks; 14 | 15 | namespace PingCastleCloud.Credentials 16 | { 17 | public interface IAzureCredential 18 | { 19 | string Tenantid { get; } 20 | string TenantidToQuery { get; set; } 21 | Task GetToken() where T : IAzureService; 22 | Token LastTokenQueried { get; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Tokens/JwtPayload.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.Common; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Text; 12 | using System.Threading.Tasks; 13 | 14 | namespace PingCastleCloud.Tokens 15 | { 16 | public class JwtPayload : JsonSerialization 17 | { 18 | public string aud { get; set; } 19 | public long exp { get; set; } 20 | public string iss { get; set; } 21 | 22 | 23 | public string jti { get; set; } 24 | public long nbf { get; set; } 25 | public string sub { get; set; } 26 | public long iat { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /PingCastleCloud.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2012 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PingCastleCloud", "PingCastleCloud.csproj", "{138C3C69-1245-4BBA-87B5-075E7140B826}" 5 | EndProject 6 | Global 7 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 8 | Debug|Any CPU = Debug|Any CPU 9 | Release|Any CPU = Release|Any CPU 10 | EndGlobalSection 11 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 12 | {138C3C69-1245-4BBA-87B5-075E7140B826}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 13 | {138C3C69-1245-4BBA-87B5-075E7140B826}.Debug|Any CPU.Build.0 = Debug|Any CPU 14 | {138C3C69-1245-4BBA-87B5-075E7140B826}.Release|Any CPU.ActiveCfg = Release|Any CPU 15 | {138C3C69-1245-4BBA-87B5-075E7140B826}.Release|Any CPU.Build.0 = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(SolutionProperties) = preSolution 18 | HideSolutionNode = FALSE 19 | EndGlobalSection 20 | EndGlobal 21 | -------------------------------------------------------------------------------- /Rules/UserConsentCompanyData.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.Data; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Text; 12 | using System.Threading.Tasks; 13 | 14 | namespace PingCastleCloud.Rules 15 | { 16 | [RuleModel("UserConsentCompanyData")] 17 | [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] 18 | [RuleMaturityLevel(2)] 19 | public class UserConsentCompanyData : RuleBase 20 | { 21 | protected override int? AnalyzeDataNew(HealthCheckCloudData healthCheckCloudData) 22 | { 23 | if (healthCheckCloudData.UsersPermissionToUserConsentToAppEnabled == true) 24 | { 25 | AddRawDetail("true"); 26 | } 27 | return null; 28 | } 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Rules/UserRegisterApplications.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.Data; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Text; 12 | using System.Threading.Tasks; 13 | 14 | namespace PingCastleCloud.Rules 15 | { 16 | [RuleModel("UserRegisterApplications")] 17 | [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] 18 | [RuleMaturityLevel(2)] 19 | public class UserRegisterApplications : RuleBase 20 | { 21 | protected override int? AnalyzeDataNew(HealthCheckCloudData healthCheckCloudData) 22 | { 23 | if (healthCheckCloudData.UsersPermissionToCreateLOBAppsEnabled == true) 24 | { 25 | AddRawDetail("true"); 26 | } 27 | return null; 28 | } 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Template/ReportBase.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | $(window).scroll(function () { 3 | if ($(window).scrollTop() >= 70) { 4 | $('.information-bar').removeClass('hidden'); 5 | $('.information-bar').fadeIn('fast'); 6 | } else { 7 | $('.information-bar').fadeOut('fast'); 8 | } 9 | }); 10 | }); 11 | 12 | document.addEventListener('DOMContentLoaded', function () { 13 | 14 | $('.div_model').on('click', function (e) { 15 | $('.div_model').not(this).popover('hide'); 16 | }); 17 | 18 | new bootstrap.Tooltip(document.body, { 19 | selector: '.has-tooltip' 20 | }); 21 | 22 | }); 23 | 24 | function getData(dataSelect) { 25 | try { 26 | const inlineJsonElement = document.querySelector( 27 | 'script[type="application/json"][data-pingcastle-selector="' + dataSelect + '"]' 28 | ); 29 | const data = JSON.parse(inlineJsonElement.textContent); 30 | return data; 31 | } catch (err) { 32 | console.error("Couldn't read JSON data from " + dataSelect, err); 33 | } 34 | } -------------------------------------------------------------------------------- /Rules/GuestUserAccessRestriction2.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.Data; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Text; 12 | using System.Threading.Tasks; 13 | 14 | namespace PingCastleCloud.Rules 15 | { 16 | [RuleModel("GuestUserAccessRestriction2")] 17 | [RuleComputation(RuleComputationType.TriggerOnPresence, 0)] 18 | [RuleMaturityLevel(4)] 19 | public class GuestUserAccessRestriction2 : RuleBase 20 | { 21 | protected override int? AnalyzeDataNew(HealthCheckCloudData healthCheckCloudData) 22 | { 23 | if (string.Equals(healthCheckCloudData.PolicyGuestUserRoleId, "10dae51f-b6af-4016-8d66-8c2a99b929b3", StringComparison.OrdinalIgnoreCase)) 24 | { 25 | AddRawDetail(healthCheckCloudData.PolicyGuestUserRoleId); 26 | } 27 | return null; 28 | } 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Rules/GuestUserAccessRestriction1.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.Data; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Text; 12 | using System.Threading.Tasks; 13 | 14 | namespace PingCastleCloud.Rules 15 | { 16 | [RuleModel("GuestUserAccessRestriction1")] 17 | [RuleComputation(RuleComputationType.TriggerOnPresence, 25)] 18 | [RuleMaturityLevel(1)] 19 | public class GuestUserAccessRestriction1 : RuleBase 20 | { 21 | protected override int? AnalyzeDataNew(HealthCheckCloudData healthCheckCloudData) 22 | { 23 | if (string.Equals(healthCheckCloudData.PolicyGuestUserRoleId, "a0b1b346-4d3e-4e8b-98f8-753987be4970", StringComparison.OrdinalIgnoreCase)) 24 | { 25 | AddRawDetail(healthCheckCloudData.PolicyGuestUserRoleId); 26 | } 27 | return null; 28 | } 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Logs/LoggingHandler.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Net.Http; 11 | using System.Text; 12 | using System.Threading; 13 | using System.Threading.Tasks; 14 | 15 | namespace PingCastleCloud.Logs 16 | { 17 | public class LoggingHandler : DelegatingHandler 18 | { 19 | private SazGenerator _sazGenerator; 20 | 21 | public LoggingHandler(SazGenerator sazGenerator, HttpMessageHandler innerHandler) 22 | : base(innerHandler) 23 | { 24 | _sazGenerator = sazGenerator; 25 | } 26 | 27 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 28 | { 29 | int num = _sazGenerator.RecordBeginQuery(request); 30 | 31 | HttpResponseMessage response = await base.SendAsync(request, cancellationToken); 32 | 33 | _sazGenerator.RecordEndQuery(num, response); 34 | 35 | return response; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tokens/TokenCache.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using System; 8 | using System.Collections.Generic; 9 | using System.IO; 10 | using System.Linq; 11 | using System.Text; 12 | using System.Threading.Tasks; 13 | 14 | namespace PingCastleCloud.Tokens 15 | { 16 | public class TokenCache 17 | { 18 | const string DirectoryName = "Cache"; 19 | public static List GetTokens() 20 | { 21 | var output = new List(); 22 | foreach (var file in Directory.EnumerateFiles(DirectoryName, "*.json")) 23 | { 24 | output.Add(Token.LoadFromString(File.ReadAllText(file))); 25 | } 26 | return output; 27 | } 28 | 29 | public static void Save(Token token) 30 | { 31 | if (!Directory.Exists(DirectoryName)) 32 | Directory.CreateDirectory(DirectoryName); 33 | File.WriteAllText(Path.Combine(DirectoryName, token .resource.Replace("https://", "").Replace("/", "") + "-" + Guid.NewGuid() + ".json"), token.ToJsonString()); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /PublicServices/UserRealmCTRequest.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.Common; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Runtime.Serialization; 12 | using System.Text; 13 | using System.Threading.Tasks; 14 | 15 | namespace PingCastleCloud.PublicServices 16 | { 17 | [DataContractAttribute] 18 | public class UserRealmCTRequest : JsonSerialization 19 | { 20 | [DataMember] 21 | public string username { get; set; } 22 | [DataMember] 23 | public bool isOtherIdpSupported { get; set; } 24 | [DataMember] 25 | public bool checkPhones { get; set; } 26 | [DataMember] 27 | public bool isRemoteNGCSupported { get; set; } 28 | [DataMember] 29 | public bool isCookieBannerShown { get; set; } 30 | [DataMember] 31 | public bool isFidoSupported { get; set; } 32 | [DataMember] 33 | public string originalRequest { get; set; } 34 | [DataMember] 35 | public string flowToken { get; set; } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /PublicServices/UserRealmV1.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.Common; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Runtime.Serialization; 12 | using System.Text; 13 | using System.Threading.Tasks; 14 | 15 | namespace PingCastleCloud.PublicServices 16 | { 17 | [DataContractAttribute] 18 | public class UserRealmV1 : JsonSerialization 19 | { 20 | [DataMember] 21 | public string ver { get; set; } 22 | [DataMember] 23 | public string account_type { get; set; } 24 | [DataMember] 25 | public string domain_name { get; set; } 26 | [DataMember] 27 | public string federation_protocol { get; set; } 28 | [DataMember] 29 | public string federation_metadata_url { get; set; } 30 | [DataMember] 31 | public string federation_active_auth_url { get; set; } 32 | [DataMember] 33 | public string cloud_instance_name { get; set; } 34 | [DataMember] 35 | public string cloud_audience_urn { get; set; } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /PublicServices/UserRealmV2.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.Common; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Runtime.Serialization; 12 | using System.Text; 13 | using System.Threading.Tasks; 14 | 15 | namespace PingCastleCloud.PublicServices 16 | { 17 | [DataContractAttribute] 18 | public class UserRealmV2 : JsonSerialization 19 | { 20 | [DataMember] 21 | public string NameSpaceType { get; set; } 22 | [DataMember] 23 | public string federation_protocol { get; set; } 24 | [DataMember] 25 | public string Login { get; set; } 26 | [DataMember] 27 | public string AuthURL { get; set; } 28 | [DataMember] 29 | public string DomainName { get; set; } 30 | [DataMember] 31 | public string FederationBrandName { get; set; } 32 | [DataMember] 33 | public List TenantBrandingInfo { get; set; } 34 | [DataMember] 35 | public string cloud_instance_name { get; set; } 36 | } 37 | 38 | 39 | } 40 | -------------------------------------------------------------------------------- /Rules/ADConnectVersion1.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.Data; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Text; 12 | using System.Threading.Tasks; 13 | 14 | namespace PingCastleCloud.Rules 15 | { 16 | [RuleModel("ADConnectVersion1")] 17 | [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] 18 | [RuleMaturityLevel(2)] 19 | public class ADConnectVersion1 : RuleBase 20 | { 21 | protected override int? AnalyzeDataNew(HealthCheckCloudData healthCheckCloudData) 22 | { 23 | if (healthCheckCloudData.ProvisionDirectorySynchronizationStatus == "Enabled") 24 | { 25 | Version v; 26 | if (Version.TryParse(healthCheckCloudData.ProvisionDirSyncClientVersion, out v)) 27 | { 28 | if (v.Major == 1) 29 | { 30 | AddRawDetail(healthCheckCloudData.ProvisionDirSyncClientVersion); 31 | } 32 | } 33 | } 34 | return null; 35 | } 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /PublicServices/UserRealmSRF.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.Common; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Runtime.Serialization; 12 | using System.Text; 13 | using System.Threading.Tasks; 14 | 15 | namespace PingCastleCloud.PublicServices 16 | { 17 | [DataContractAttribute] 18 | public class UserRealmSRF : JsonSerialization 19 | { 20 | [DataMember] 21 | public int State { get; set; } 22 | [DataMember] 23 | public int UserState { get; set; } 24 | [DataMember] 25 | public string Login { get; set; } 26 | [DataMember] 27 | public string NameSpaceType { get; set; } 28 | [DataMember] 29 | public string DomainName { get; set; } 30 | [DataMember] 31 | public int FederationGlobalVersion { get; set; } 32 | [DataMember] 33 | public string AuthURL { get; set; } 34 | [DataMember] 35 | public string FederationBrandName { get; set; } 36 | [DataMember] 37 | public string CloudInstanceName { get; set; } 38 | [DataMember] 39 | public string CloudInstanceIssuerUri { get; set; } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /RESTServices/AzureServiceAttribute.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | using System.Reflection; 13 | 14 | namespace PingCastleCloud.RESTServices.Azure 15 | { 16 | public class AzureServiceAttribute : Attribute 17 | { 18 | public AzureServiceAttribute(string ClientID, string Resource, string RedirectUri = Constants.redirectUri) 19 | { 20 | this.ClientID = Guid.Parse(ClientID); 21 | this.Resource = Resource; 22 | this.RedirectUri = RedirectUri; 23 | } 24 | public Guid ClientID { get; set; } 25 | public string Resource { get; set; } 26 | public string RedirectUri { get; set; } 27 | 28 | public static AzureServiceAttribute GetAzureServiceAttribute() where T : IAzureService 29 | { 30 | AzureServiceAttribute[] attrs = (AzureServiceAttribute[])typeof(T).GetCustomAttributes(typeof(AzureServiceAttribute)); 31 | if (attrs.Length > 0) 32 | return attrs[0]; 33 | throw new ApplicationException("no service attribute found"); 34 | } 35 | } 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /Tokens/Token.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.Common; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.IO; 11 | using System.Linq; 12 | using System.Runtime.Serialization; 13 | using System.Runtime.Serialization.Json; 14 | using System.Text; 15 | using System.Threading.Tasks; 16 | 17 | namespace PingCastleCloud.Tokens 18 | { 19 | public class Token : JsonSerialization 20 | { 21 | 22 | public string token_type { get; set; } 23 | 24 | public string scope { get; set; } 25 | 26 | public uint expires_in { get; set; } 27 | 28 | public uint ext_expires_in { get; set; } 29 | 30 | public uint expires_on { get; set; } 31 | 32 | public uint not_before { get; set; } 33 | 34 | 35 | public string resource { get; set; } 36 | 37 | public string access_token { get; set; } 38 | 39 | public string refresh_token { get; set; } 40 | 41 | public string id_token { get; set; } 42 | 43 | public JwtToken ToJwtToken() 44 | { 45 | return JwtToken.LoadFromBase64String(access_token.Split('.')[1]); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /RESTServices/EndPointAttribute.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | using System.Reflection; 13 | 14 | namespace PingCastleCloud.RESTServices.Azure 15 | { 16 | public class EndPointAttribute : Attribute 17 | { 18 | public EndPointAttribute(string Authorize, string Token) 19 | { 20 | this.AuthorizeEndPoint = Authorize; 21 | this.TokenEndPoint = Token; 22 | } 23 | 24 | public string AuthorizeEndPoint { get; private set; } 25 | public string TokenEndPoint { get; private set; } 26 | 27 | public static EndPointAttribute GetEndPointAttribute() where T : IAzureService 28 | { 29 | EndPointAttribute[] attrs = (EndPointAttribute[])typeof(T).GetCustomAttributes(typeof(EndPointAttribute)); 30 | if (attrs.Length > 0) 31 | return attrs[0]; 32 | return DefaultEndPointAttribute(); 33 | } 34 | 35 | private static EndPointAttribute DefaultEndPointAttribute() 36 | { 37 | return new EndPointAttribute(Constants.OAuth2AuthorizeEndPoint, Constants.OAuth2TokenEndPoint); 38 | } 39 | } 40 | 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Constants.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | 13 | namespace PingCastleCloud 14 | { 15 | public class Constants 16 | { 17 | public const string LoginEndPoint = "https://login.microsoftonline.com/"; 18 | public const string OAuth2AuthorizeEndPoint = "https://login.microsoftonline.com/common/oauth2/authorize"; 19 | public const string OAuth2TokenEndPoint = "https://login.microsoftonline.com/common/oauth2/token"; 20 | public const string OAuth2NativeClientEndPoint = "urn:ietf:wg:oauth:2.0:oob";//"https://login.microsoftonline.com/common/oauth2/nativeclient"; 21 | public const string UserRealmEndpoint = "https://login.microsoftonline.com/common/userrealm/"; 22 | public const string UserRealmSRFEndpoint = "https://login.microsoftonline.com/GetUserRealm.srf?login="; 23 | public const string UserRealmCTEndpoint = "https://login.microsoftonline.com/common/GetCredentialType"; 24 | public const string OpenIdConfigurationEndpoint = "https://login.windows.net/xxxxx/.well-known/openid-configuration"; 25 | public const string ProvisionningEndpoint = "https://provisioningapi.microsoftonline.com/provisioningwebservice.svc"; 26 | public const string redirectUri = "urn:ietf:wg:oauth:2.0:oob"; 27 | public const string OrganisationsNativeClient = "https://login.microsoftonline.com/organizations/oauth2/nativeclient"; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Template/ProcessTemplate.ps1: -------------------------------------------------------------------------------- 1 | # thanks Mathias ! (@IISResetMe) 2 | 3 | function New-Gzip 4 | { 5 | param( 6 | [Parameter(Mandatory = $true)] 7 | [ValidateScript({Test-Path $_ -PathType Leaf})] 8 | [string]$Path 9 | ) 10 | 11 | if((Resolve-Path $Path).Provider.Name -ne 'FileSystem'){ 12 | Write-Error "$Path is not a file..." 13 | return 14 | } 15 | 16 | $sourceItem = Get-Item $Path 17 | $targetName = $sourceItem.Name + '.gz' 18 | 19 | try{ 20 | $sourceStream = $sourceItem.OpenRead() 21 | $targetStream = New-Object IO.FileStream $targetName ,'Create','Write','Read' 22 | $gzipStream = [System.IO.Compression.GZipStream]::new($targetStream, [System.IO.Compression.CompressionMode]::Compress) 23 | $sourceStream.CopyTo($gzipStream) 24 | } 25 | catch{ 26 | throw 27 | } 28 | finally{ 29 | $gzipStream,$targetStream,$sourceStream |ForEach-Object { 30 | if($_){ 31 | $_.Dispose() 32 | } 33 | } 34 | } 35 | 36 | return Get-Item $targetName 37 | } 38 | 39 | function ProcessTemplate 40 | { 41 | param( 42 | [Parameter(Mandatory)] 43 | [string]$File = 'file.js', 44 | 45 | [switch]$PassThru 46 | ) 47 | Write-Host "Processing " $File 48 | $js = Get-Item $File -ErrorAction SilentlyContinue 49 | if(-not $?){ 50 | Write-Error "$File not found" 51 | return 52 | } 53 | 54 | $gz = Get-Item "${File}.gz" -ErrorAction SilentlyContinue 55 | 56 | if(-not $? -or $js.LastWriteTime -ge $gz.LastWriteTime){ 57 | Write-Host "Creating " $File ".gz" 58 | $gz = New-Gzip -Path $js.FullName 59 | } 60 | 61 | if($PassThru){ 62 | return $gz 63 | } 64 | } 65 | 66 | ProcessTemplate -File responsivetemplate.html 67 | ProcessTemplate -File ReportBase.css 68 | ProcessTemplate -File ReportBase.js 69 | ProcessTemplate -File ReportMain.js 70 | -------------------------------------------------------------------------------- /Rules/ADConnectVersion.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.Data; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Text; 12 | using System.Threading.Tasks; 13 | 14 | namespace PingCastleCloud.Rules 15 | { 16 | [RuleModel("ADConnectVersion")] 17 | [RuleComputation(RuleComputationType.TriggerOnPresence, 15)] 18 | [RuleMaturityLevel(1)] 19 | [RuleMitreAttackTechnique(MitreAttackTechnique.ManintheMiddle)] 20 | public class ADConnectVersion : RuleBase 21 | { 22 | protected override int? AnalyzeDataNew(HealthCheckCloudData healthCheckCloudData) 23 | { 24 | if (healthCheckCloudData.ProvisionDirectorySynchronizationStatus == "Enabled") 25 | { 26 | Version v; 27 | if (Version.TryParse(healthCheckCloudData.ProvisionDirSyncClientVersion, out v)) 28 | { 29 | if (v.Major == 1) 30 | { 31 | if (v < new Version(1, 6, 11, 3)) 32 | { 33 | AddRawDetail(healthCheckCloudData.ProvisionDirSyncClientVersion); 34 | } 35 | } 36 | else if (v.Major == 2) 37 | { 38 | if (v < new Version(2, 0, 8, 0)) 39 | { 40 | AddRawDetail(healthCheckCloudData.ProvisionDirSyncClientVersion); 41 | } 42 | } 43 | } 44 | } 45 | return null; 46 | } 47 | 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using System.Reflection; 8 | using System.Runtime.CompilerServices; 9 | using System.Runtime.InteropServices; 10 | 11 | // Les informations générales relatives à un assembly dépendent de 12 | // l'ensemble d'attributs suivant. Changez les valeurs de ces attributs pour modifier les informations 13 | // associées à un assembly. 14 | [assembly: AssemblyTitle("PingCastleCloud")] 15 | [assembly: AssemblyDescription("")] 16 | [assembly: AssemblyConfiguration("")] 17 | [assembly: AssemblyCompany("")] 18 | [assembly: AssemblyProduct("PingCastleCloud")] 19 | [assembly: AssemblyCopyright("Copyright © 2022")] 20 | [assembly: AssemblyTrademark("")] 21 | [assembly: AssemblyCulture("")] 22 | 23 | // L'affectation de la valeur false à ComVisible rend les types invisibles dans cet assembly 24 | // aux composants COM. Si vous devez accéder à un type dans cet assembly à partir de 25 | // COM, affectez la valeur true à l'attribut ComVisible sur ce type. 26 | [assembly: ComVisible(false)] 27 | 28 | // Le GUID suivant est pour l'ID de la typelib si ce projet est exposé à COM 29 | [assembly: Guid("123099d2-462c-4712-af04-d9455461e1ba")] 30 | 31 | // Les informations de version pour un assembly se composent des quatre valeurs suivantes : 32 | // 33 | // Version principale 34 | // Version secondaire 35 | // Numéro de build 36 | // Révision 37 | // 38 | // Vous pouvez spécifier toutes les valeurs ou indiquer les numéros de build et de révision par défaut 39 | // en utilisant '*', comme indiqué ci-dessous : 40 | // [assembly: AssemblyVersion("1.0.*")] 41 | [assembly: AssemblyVersion("1.0.0.0")] 42 | [assembly: AssemblyFileVersion("1.0.0.0")] 43 | -------------------------------------------------------------------------------- /RESTServices/Azure/AzureADConnectApi.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using Newtonsoft.Json; 8 | using PingCastleCloud.Credentials; 9 | using PingCastleCloud.Tokens; 10 | using System; 11 | using System.Collections.Generic; 12 | using System.Collections.Specialized; 13 | using System.Linq; 14 | using System.Net.Http; 15 | using System.Text; 16 | using System.Threading.Tasks; 17 | using System.Web; 18 | 19 | namespace PingCastleCloud.RESTServices.Azure 20 | { 21 | [AzureService("d3590ed6-52b3-4102-aeff-aad2292ab01c", "74658136-14ec-4630-ad9b-26e160ff0fc6")] 22 | public class AzureADConnectApi : RESTClientBase, IAzureService 23 | { 24 | public AzureADConnectApi(IAzureCredential credential) : base(credential) 25 | { 26 | } 27 | 28 | protected override string BuidEndPoint(string function, string optionalQuery) 29 | { 30 | var query = HttpUtility.ParseQueryString(optionalQuery); 31 | query["api-version"] = "2.0"; 32 | 33 | var builder = new UriBuilder("https://main.iam.ad.ext.azure.com/api/Directories/" + function); 34 | builder.Query = query.ToString(); 35 | return builder.ToString(); 36 | } 37 | 38 | public ADConnectStatusResponse ADConnectStatus() 39 | { 40 | return CallEndPoint("ADConnectStatus"); 41 | } 42 | 43 | public class ADConnectStatusResponse 44 | { 45 | public int verifiedDomainCount { get; set; } 46 | public int verifiedCustomDomainCount { get; set; } 47 | public int federatedDomainCount { get; set; } 48 | public int? numberOfHoursFromLastSync { get; set; } 49 | public bool? dirSyncEnabled { get; set; } 50 | public bool? dirSyncConfigured { get; set; } 51 | public bool? passThroughAuthenticationEnabled { get; set; } 52 | public bool? seamlessSingleSignOnEnabled { get; set; } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /CreateSecureAppCert.ps1: -------------------------------------------------------------------------------- 1 | #======================================================================================= 2 | #region PARAMETERS 3 | #======================================================================================= 4 | Param( 5 | [string]$TenantName, 6 | [string]$ExportPrivateKey, 7 | [string]$ExportPath 8 | ) 9 | #endregion 10 | #======================================================================================= 11 | 12 | $DateTime = (Get-Date -Format "yyyyMMdd-HHmmss").tostring() 13 | $ShortTenantName = ($TenantName -split "\.")[0] 14 | 15 | # Where to export the certificate without the private key 16 | $CerOutputPath = "$ExportPath\$DateTime-$ShortTenantName-AppCert" 17 | 18 | # Expiration date of the new certificate 19 | $ExpirationDate = (Get-Date).AddYears(2) 20 | 21 | # Splat for readability 22 | $CreateCertificateSplat = @{ 23 | FriendlyName = "PSCC-$TenantName-App" 24 | DnsName = $TenantName 25 | CertStoreLocation = "Cert:\CurrentUser\My" 26 | NotAfter = $ExpirationDate 27 | KeyExportPolicy = "Exportable" 28 | KeySpec = "Signature" 29 | Provider = "Microsoft Enhanced RSA and AES Cryptographic Provider" 30 | HashAlgorithm = "SHA256" 31 | } 32 | 33 | # Create certificate 34 | $Certificate = New-SelfSignedCertificate @CreateCertificateSplat 35 | 36 | # Get certificate path 37 | $CertificatePath = Join-Path -Path "Cert:\CurrentUser\My" -ChildPath $Certificate.Thumbprint 38 | 39 | # Export certificate without private key 40 | Export-Certificate -Cert $CertificatePath -FilePath "$CerOutputPath.cer" | Out-Null 41 | if($ExportPrivateKey -eq $true) 42 | { 43 | Write-Host "Please create a password for the certificate which will be exported with the private key: " -ForegroundColor Yellow -NoNewline 44 | $Password = Read-Host -AsSecureString 45 | # Export certificate with private key 46 | Export-PfxCertificate -Cert $CertificatePath -FilePath "$CerOutputPath.pfx" -Password $Password | Out-Null 47 | } 48 | 49 | Write-Host "Certificate validity : $($Certificate.NotBefore) through $($Certificate.NotAfter) " 50 | Write-Host "Certificate thumbprint : $($Certificate.Thumbprint)" 51 | Write-Host "Certificate exported to : $CerOutputPath" 52 | 53 | -------------------------------------------------------------------------------- /Tokens/JwtToken.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.Common; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.IO; 11 | using System.Linq; 12 | using System.Runtime.Serialization; 13 | using System.Runtime.Serialization.Json; 14 | using System.Text; 15 | using System.Threading.Tasks; 16 | 17 | namespace PingCastleCloud.Tokens 18 | { 19 | public class JwtToken : JsonSerialization 20 | { 21 | 22 | public string request_nonce { get; set; } 23 | 24 | public string refresh_token { get; set; } 25 | 26 | public string aud { get; set; } 27 | 28 | public string iss { get; set; } 29 | 30 | public long iat { get; set; } 31 | 32 | public long nbf { get; set; } 33 | 34 | public long exp { get; set; } 35 | 36 | public string act { get; set; } 37 | 38 | public string aio { get; set; } 39 | 40 | public List amr { get; set; } 41 | 42 | public string appid { get; set; } 43 | 44 | public string appidacr { get; set; } 45 | 46 | public string deviceid { get; set; } 47 | 48 | public string family_name { get; set; } 49 | 50 | public string given_name { get; set; } 51 | 52 | public string ipaddr { get; set; } 53 | 54 | public string name { get; set; } 55 | 56 | public string oid { get; set; } 57 | 58 | public string onprem_sid { get; set; } 59 | 60 | public string puid { get; set; } 61 | 62 | public string rh { get; set; } 63 | 64 | public string scp { get; set; } 65 | 66 | public string sub { get; set; } 67 | 68 | public string tenant_region_scope { get; set; } 69 | 70 | public string tid { get; set; } 71 | 72 | public string unique_name { get; set; } 73 | 74 | public string upn { get; set; } 75 | 76 | public string uti { get; set; } 77 | 78 | public string ver { get; set; } 79 | 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Credentials/CredentialBase.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.RESTServices.Azure; 8 | using PingCastleCloud.Tokens; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Linq; 12 | using System.Text; 13 | using System.Threading.Tasks; 14 | 15 | namespace PingCastleCloud.Credentials 16 | { 17 | public abstract class CredentialBase : IAzureCredential 18 | { 19 | public CredentialBase() : this(null) 20 | { 21 | 22 | } 23 | public CredentialBase(string tenantid) 24 | { 25 | this.tenantId = tenantid; 26 | } 27 | 28 | Dictionary cache = new Dictionary(); 29 | public Token LastTokenQueried { get; protected set; } 30 | public async Task GetToken() where T : IAzureService 31 | { 32 | Token token; 33 | if (cache.ContainsKey(typeof(T))) 34 | { 35 | token = cache[typeof(T)]; 36 | 37 | // TODO refresh 38 | 39 | return token; 40 | } 41 | token = await TokenFactory.GetToken(this); 42 | LastTokenQueried = token; 43 | cache[typeof(T)] = token; 44 | return token; 45 | } 46 | string tenantId; 47 | public string Tenantid 48 | { 49 | get 50 | { 51 | if (string.IsNullOrEmpty(tenantId) && cache.Count > 0) 52 | { 53 | var token = cache.Values.FirstOrDefault(); 54 | tenantId = token.ToJwtToken().tid; 55 | } 56 | return tenantId; 57 | } 58 | } 59 | 60 | string tenantidToQuery; 61 | public string TenantidToQuery 62 | { 63 | get 64 | { 65 | return tenantidToQuery; 66 | } 67 | set 68 | { 69 | if (tenantidToQuery == value) 70 | return; 71 | if (string.IsNullOrEmpty(value)) 72 | return; 73 | tenantidToQuery = value; 74 | cache.Clear(); 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /PublicServices/UserRealmCT.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.Common; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Runtime.Serialization; 12 | using System.Text; 13 | using System.Threading.Tasks; 14 | 15 | namespace PingCastleCloud.PublicServices 16 | { 17 | [DataContractAttribute] 18 | public class UserRealmCT : JsonSerialization 19 | { 20 | [DataMember] 21 | public string Username { get; set; } 22 | [DataMember] 23 | public string Display { get; set; } 24 | [DataMember] 25 | public int IfExistsResult { get; set; } 26 | [DataMember] 27 | public bool IsUnmanaged { get; set; } 28 | [DataMember] 29 | public int ThrottleStatus { get; set; } 30 | [DataMember] 31 | public UserRealmCTCredentials Credentials { get; set; } 32 | [DataMember] 33 | public UserRealmCTEstsProperties EstsProperties { get; set; } 34 | [DataMember] 35 | public string FlowToken { get; set; } 36 | [DataMember] 37 | public bool IsSignupDisallowed { get; set; } 38 | [DataMember] 39 | public string apiCanary { get; set; } 40 | } 41 | 42 | public class UserRealmCTCredentials 43 | { 44 | [DataMember] 45 | public int PrefCredential { get; set; } 46 | [DataMember] 47 | public bool HasPassword { get; set; } 48 | [DataMember] 49 | public string RemoteNgcParams { get; set; } 50 | [DataMember] 51 | public string FidoParams { get; set; } 52 | [DataMember] 53 | public string SasParams { get; set; } 54 | [DataMember] 55 | public string CertAuthParams { get; set; } 56 | [DataMember] 57 | public string GoogleParams { get; set; } 58 | [DataMember] 59 | public string FacebookParams { get; set; } 60 | [DataMember] 61 | public string FederationRedirectUrl { get; set; } 62 | } 63 | 64 | public class UserRealmCTEstsProperties 65 | { 66 | [DataMember] 67 | public List UserTenantBranding { get; set; } 68 | [DataMember] 69 | public int DomainType { get; set; } 70 | 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Tokens/CookieManager.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Runtime.InteropServices; 11 | using System.Text; 12 | using System.Threading.Tasks; 13 | 14 | namespace PingCastleCloud.RESTServices 15 | { 16 | [StructLayout(LayoutKind.Sequential)] 17 | public struct ProofOfPossessionCookieInfo 18 | { 19 | [MarshalAs(UnmanagedType.LPWStr)] 20 | public string Name; 21 | [MarshalAs(UnmanagedType.LPWStr)] 22 | public string Data; 23 | public readonly uint Flags; 24 | [MarshalAs(UnmanagedType.LPWStr)] 25 | public string P3PHeader; 26 | } 27 | 28 | 29 | // All these are defined in the Win10 WDK 30 | [Guid("CDAECE56-4EDF-43DF-B113-88E4556FA1BB")] 31 | [ComImport] 32 | [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 33 | internal interface IProofOfPossessionCookieInfoManager 34 | { 35 | int GetCookieInfoForUri( 36 | [MarshalAs(UnmanagedType.LPWStr)] string Uri, 37 | out uint cookieInfoCount, 38 | out IntPtr cookieInfo 39 | ); 40 | } 41 | 42 | [Guid("A9927F85-A304-4390-8B23-A75F1C668600")] 43 | [ComImport] 44 | public class WindowsTokenProvider 45 | { 46 | } 47 | 48 | 49 | public class CookieInfoManager 50 | { 51 | public static List GetCookieInforForUri(string uri) 52 | { 53 | var output = new List(); 54 | var provider = (IProofOfPossessionCookieInfoManager)new WindowsTokenProvider(); 55 | IntPtr ptr; 56 | uint count; 57 | var error = provider.GetCookieInfoForUri(uri, out count, out ptr); 58 | if (error != 0) 59 | throw new COMException("unable to call GetCookieInfoForUri", error); 60 | var offset = ptr; 61 | for (int i = 0; i < count; i++) 62 | { 63 | var info = (ProofOfPossessionCookieInfo)Marshal.PtrToStructure(offset, typeof(ProofOfPossessionCookieInfo)); 64 | output.Add(info); 65 | } 66 | 67 | Marshal.FreeCoTaskMem(ptr); 68 | return output; 69 | } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /PublicServices/OpenIDConfiguration.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.Common; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Runtime.Serialization; 12 | using System.Text; 13 | using System.Threading.Tasks; 14 | 15 | namespace PingCastleCloud.PublicServices 16 | { 17 | [DataContractAttribute] 18 | public class OpenIDConfiguration : JsonSerialization 19 | { 20 | [DataMember] 21 | public string token_endpoint { get; set; } 22 | [DataMember] 23 | public string[] token_endpoint_auth_methods_supported { get; set; } 24 | [DataMember] 25 | public string[] response_modes_supported { get; set; } 26 | [DataMember] 27 | public string[] subject_types_supported { get; set; } 28 | [DataMember] 29 | public string[] id_token_signing_alg_values_supported { get; set; } 30 | [DataMember] 31 | public string[] response_types_supported { get; set; } 32 | [DataMember] 33 | public string[] scopes_supported { get; set; } 34 | [DataMember] 35 | public string issuer { get; set; } 36 | [DataMember] 37 | public bool microsoft_multi_refresh_token { get; set; } 38 | [DataMember] 39 | public string authorization_endpoint { get; set; } 40 | [DataMember] 41 | public string device_authorization_endpoint { get; set; } 42 | [DataMember] 43 | public bool http_logout_supported { get; set; } 44 | [DataMember] 45 | public bool frontchannel_logout_supported { get; set; } 46 | [DataMember] 47 | public string end_session_endpoint { get; set; } 48 | [DataMember] 49 | public string[] claims_supported { get; set; } 50 | [DataMember] 51 | public string check_session_iframe { get; set; } 52 | [DataMember] 53 | public string userinfo_endpoint { get; set; } 54 | [DataMember] 55 | public string kerberos_endpoint { get; set; } 56 | [DataMember] 57 | public string tenant_region_scope { get; set; } 58 | [DataMember] 59 | public string cloud_instance_name { get; set; } 60 | [DataMember] 61 | public string cloud_graph_host_name { get; set; } 62 | [DataMember] 63 | public string msgraph_host { get; set; } 64 | [DataMember] 65 | public string rbac_url { get; set; } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ping Castle Cloud 2 | 3 | important: the source code has been moved to the main PingCastle project. 4 | this project is now archived. 5 | 6 | 7 | ## Introduction 8 | 9 | Ping Castle Cloud is a tool designed to assess quickly the AzureAD security level with a methodology based on risk assessment and a maturity framework. 10 | It does not aim at a perfect evaluation but rather as an efficiency compromise. 11 | It is inspired from the [Ping Castle project](https://pingcastle.com/) 12 | 13 | ``` 14 | \==--O___ PingCastle Cloud (Version 1.0.0.0 17/07/2022 18:58:40) 15 | \ / \ ¨¨> Get Active Directory Security at 80% in 20% of the time 16 | \/ \ ,' End of support: 31/07/2023 17 | O¨---O 18 | \ ,' Vincent LE TOUX (contact@pingcastle.com) 19 | v twitter: @mysmartlogon https://www.pingcastle.com 20 | What do you want to do? 21 | ======================= 22 | Using interactive mode. 23 | Do not forget that there are other command line switches like --help that you can use 24 | 1-healthcheck -Score the risk of a domain 25 | 2-exportasguest-Export users and group as a Guest 26 | 3-advanced -Open the advanced menu 27 | 0-Exit 28 | 29 | ``` 30 | 31 | Check https://www.pingcastle.com for the documentation and methodology 32 | 33 | ## Build 34 | 35 | PingCastle is a c# project which can be build from Visual Studio 2019 36 | 37 | ## Support & lifecycle 38 | 39 | For support requests, you should contact support@pingcastle.com 40 | The support for the basic edition is made on a best effort basis and fixes delivered when a new version is delivered. 41 | 42 | The Basic Edition of PingCastleCloud is released every 6 months (January, August) and this repository is updated at each release. 43 | 44 | If you need changes, please contact contact@pingcastle.com for support packages. 45 | 46 | ## License 47 | 48 | PingCastleCloud source code is licensed under a proprietary license and the Non-Profit Open Software License ("Non-Profit OSL") 3.0. 49 | 50 | Except if a license is purchased, you are not allowed to make any profit from this source code. 51 | To be more specific: 52 | * It is allowed to run PingCastle without purchasing any license on for profit companies if the company itself (or its ITSM provider) run it. 53 | * To build services based on PingCastle AND earning money from that, you MUST purchase a license. 54 | 55 | Ping Castle uses the following Open source components: 56 | 57 | * [Bootstrap](https://getbootstrap.com/) licensed under the [MIT license](https://tldrlegal.com/license/mit-license) 58 | * [JQuery](https://jquery.org) licensed under the [MIT license](https://tldrlegal.com/license/mit-license) 59 | * [vis.js](http://visjs.org/) licensed under the [MIT license](https://tldrlegal.com/license/mit-license) 60 | 61 | ## Author 62 | 63 | Author: Vincent LE TOUX 64 | 65 | You can contact me at vincent.letoux@gmail.com 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /UI/AuthenticationDialog.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace PingCastleCloud.UI 2 | { 3 | partial class AuthenticationDialog 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Windows Form Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | this.components = new System.ComponentModel.Container(); 32 | System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(AuthenticationDialog)); 33 | this.webBrowser = new System.Windows.Forms.WebBrowser(); 34 | this.timerForAutoSignAttempt = new System.Windows.Forms.Timer(this.components); 35 | this.SuspendLayout(); 36 | // 37 | // webBrowser 38 | // 39 | this.webBrowser.Dock = System.Windows.Forms.DockStyle.Fill; 40 | this.webBrowser.Location = new System.Drawing.Point(0, 0); 41 | this.webBrowser.MinimumSize = new System.Drawing.Size(20, 20); 42 | this.webBrowser.Name = "webBrowser"; 43 | this.webBrowser.Size = new System.Drawing.Size(543, 887); 44 | this.webBrowser.TabIndex = 0; 45 | this.webBrowser.Url = new System.Uri("http://w", System.UriKind.Absolute); 46 | // 47 | // timerForAutoSignAttempt 48 | // 49 | this.timerForAutoSignAttempt.Tick += new System.EventHandler(this.timerForAutoSignAttempt_Tick); 50 | // 51 | // AuthenticationDialog 52 | // 53 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); 54 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 55 | this.ClientSize = new System.Drawing.Size(543, 887); 56 | this.Controls.Add(this.webBrowser); 57 | this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); 58 | this.Name = "AuthenticationDialog"; 59 | this.Text = "PingCastle Cloud"; 60 | this.ResumeLayout(false); 61 | 62 | } 63 | 64 | #endregion 65 | 66 | private System.Windows.Forms.WebBrowser webBrowser; 67 | private System.Windows.Forms.Timer timerForAutoSignAttempt; 68 | } 69 | } -------------------------------------------------------------------------------- /PublicServices/PublicService.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.Common; 8 | using PingCastleCloud.RESTServices; 9 | using PingCastleCloud.RESTServices.Azure; 10 | using System; 11 | using System.Collections.Generic; 12 | using System.Linq; 13 | using System.Net.Http; 14 | using System.Text; 15 | using System.Threading.Tasks; 16 | 17 | namespace PingCastleCloud.PublicServices 18 | { 19 | public class PublicService 20 | { 21 | public static async Task GetUserRealmv1(string domain) 22 | { 23 | var httpClient = HttpClientHelper.GetHttpClient(); 24 | 25 | var response = await httpClient.GetStringAsync(Constants.UserRealmEndpoint + domain + "?api-version=1.0"); 26 | 27 | return UserRealmV1.LoadFromString(response); 28 | } 29 | 30 | public static async Task GetUserRealmV2(string domain) 31 | { 32 | var httpClient = HttpClientHelper.GetHttpClient(); 33 | 34 | var response = await httpClient.GetStringAsync(Constants.UserRealmEndpoint + domain + "?api-version=2.0"); 35 | 36 | return UserRealmV2.LoadFromString(response); 37 | } 38 | 39 | public static async Task GetUserRealmSRF(string domain) 40 | { 41 | var httpClient = HttpClientHelper.GetHttpClient(); 42 | 43 | var response = await httpClient.GetStringAsync(Constants.UserRealmSRFEndpoint + domain); 44 | 45 | return UserRealmSRF.LoadFromString(response); 46 | } 47 | 48 | public static async Task GetUserRealmCT(string email) 49 | { 50 | var httpClient = HttpClientHelper.GetHttpClient(); 51 | 52 | var request = new UserRealmCTRequest 53 | { 54 | username = email, 55 | isOtherIdpSupported = true, 56 | checkPhones = true, 57 | isRemoteNGCSupported = false, 58 | isCookieBannerShown = false, 59 | isFidoSupported = false, 60 | originalRequest = "", 61 | flowToken = "" 62 | }; 63 | 64 | var stringContent = new StringContent(request.ToJsonString()); 65 | var response = await httpClient.PostAsync(Constants.UserRealmCTEndpoint, stringContent); 66 | var t = await response.Content.ReadAsStringAsync(); 67 | return UserRealmCT.LoadFromString(t); 68 | } 69 | 70 | public static async Task GetOpenIDConfiguration(string domain) 71 | { 72 | var httpClient = HttpClientHelper.GetHttpClient(); 73 | 74 | var response = await httpClient.GetStringAsync(Constants.OpenIdConfigurationEndpoint.Replace("xxxxx", domain)); 75 | 76 | return OpenIDConfiguration.LoadFromString(response); 77 | } 78 | 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Common/JsonSerialization.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using Newtonsoft.Json; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.IO; 11 | using System.Linq; 12 | using System.Text; 13 | using System.Threading.Tasks; 14 | 15 | namespace PingCastleCloud.Common 16 | { 17 | public abstract class JsonSerialization 18 | { 19 | public static T LoadFromString(string input) 20 | { 21 | /*using (var stream = new MemoryStream()) 22 | { 23 | var bytes = Encoding.UTF8.GetBytes(input); 24 | stream.Write(bytes, 0, bytes.Length); 25 | stream.Position = 0; 26 | return LoadFromStream(stream); 27 | }*/ 28 | 29 | return JsonConvert.DeserializeObject(input); 30 | } 31 | public static T LoadFromStream(Stream input) 32 | { 33 | /*T t; 34 | var serializer = new DataContractJsonSerializer(typeof(T)); 35 | 36 | t = (T)serializer.ReadObject(input); 37 | */ 38 | using (StreamReader sr = new StreamReader(input)) 39 | using (JsonReader reader = new JsonTextReader(sr)) 40 | { 41 | JsonSerializer serializer = new JsonSerializer(); 42 | return (T) serializer.Deserialize(reader, typeof(T)); 43 | } 44 | } 45 | 46 | public string ToJsonString() 47 | { 48 | return JsonConvert.SerializeObject(this); 49 | /* 50 | var serializer = new DataContractJsonSerializer(typeof(T)); 51 | using (var ms = new MemoryStream()) 52 | { 53 | serializer.WriteObject(ms, this); 54 | var buf = ms.ToArray(); 55 | return Encoding.UTF8.GetString(buf); 56 | }*/ 57 | } 58 | 59 | public string ToBase64JsonString() 60 | { 61 | var data = JsonConvert.SerializeObject(this); 62 | var payloadString = Encoding.UTF8.GetBytes(data); 63 | return Convert.ToBase64String(payloadString); 64 | } 65 | 66 | public static T LoadFromBase64String(string payload) 67 | { 68 | var payloadBytes = Convert.FromBase64String(payload.PadRight(payload.Length + (payload.Length * 3) % 4, '=')); 69 | var payloadString = Encoding.UTF8.GetString(payloadBytes); 70 | return JsonConvert.DeserializeObject(payloadString); 71 | /*T t; 72 | var serializer = new DataContractJsonSerializer(typeof(T)); 73 | using (var stream = new MemoryStream()) 74 | { 75 | stream.Write(payloadString, 0, payloadString.Length); 76 | stream.Position = 0; 77 | t = (T)serializer.ReadObject(stream); 78 | } 79 | 80 | return t;*/ 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /PublicServices/TenantBrandingInfo.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Runtime.Serialization; 11 | using System.Text; 12 | using System.Threading.Tasks; 13 | 14 | namespace PingCastleCloud.PublicServices 15 | { 16 | [DataContractAttribute] 17 | public class TenantBrandingInfo 18 | { 19 | [DataMember] 20 | public int Locale { get; set; } 21 | [DataMember] 22 | public string BannerLogo { get; set; } 23 | [DataMember] 24 | public string TileLogo { get; set; } 25 | [DataMember] 26 | public string TileDarkLogo { get; set; } 27 | [DataMember] 28 | public string Illustration { get; set; } 29 | [DataMember] 30 | public string BackgroundColor { get; set; } 31 | [DataMember] 32 | public string BoilerPlateText { get; set; } 33 | [DataMember] 34 | public string UserIdLabel { get; set; } 35 | [DataMember] 36 | public bool KeepMeSignedInDisabled { get; set; } 37 | [DataMember] 38 | public bool UseTransparentLightBox { get; set; } 39 | [DataMember] 40 | public TenantBrandingInfoLayoutTemplateConfig LayoutTemplateConfig { get; set; } 41 | [DataMember] 42 | public TenantBrandingInfoCustomizationFiles CustomizationFiles { get; set; } 43 | 44 | } 45 | 46 | [DataContractAttribute] 47 | public class TenantBrandingInfoLayoutTemplateConfig 48 | { 49 | [DataMember] 50 | public bool showHeader { get; set; } 51 | [DataMember] 52 | public string headerLogo { get; set; } 53 | [DataMember] 54 | public int layoutType { get; set; } 55 | [DataMember] 56 | public bool hideCantAccessYourAccount { get; set; } 57 | [DataMember] 58 | public bool hideForgotMyPassword { get; set; } 59 | [DataMember] 60 | public bool hideResetItNow { get; set; } 61 | [DataMember] 62 | public bool hideAccountResetCredentials { get; set; } 63 | [DataMember] 64 | public bool showFooter { get; set; } 65 | [DataMember] 66 | public bool hideTOU { get; set; } 67 | [DataMember] 68 | public bool hidePrivacy { get; set; } 69 | 70 | } 71 | 72 | [DataContractAttribute] 73 | public class TenantBrandingInfoCustomizationFiles 74 | { 75 | [DataMember] 76 | public TenantBrandingInfoCustomizationFilesStrings strings { get; set; } 77 | [DataMember] 78 | public string customCssUrl { get; set; } 79 | 80 | } 81 | 82 | [DataContractAttribute] 83 | public class TenantBrandingInfoCustomizationFilesStrings 84 | { 85 | [DataMember] 86 | public string adminConsent { get; set; } 87 | [DataMember] 88 | public string attributeCollection { get; set; } 89 | [DataMember] 90 | public string authenticatorNudgeScreen { get; set; } 91 | [DataMember] 92 | public string conditionalAccess { get; set; } 93 | 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /RESTServices/Azure/MicrosoftGraph.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.Credentials; 8 | using PingCastleCloud.RESTServices.Azure; 9 | using PingCastleCloud.Tokens; 10 | using System; 11 | using System.Collections.Generic; 12 | using System.Linq; 13 | using System.Text; 14 | using System.Threading.Tasks; 15 | using System.Web; 16 | 17 | namespace PingCastleCloud.RESTServices 18 | { 19 | [AzureService("1b730954-1685-4b74-9bfd-dac224a7b894", "https://graph.microsoft.com")] 20 | public class MicrosoftGraph : RESTClientBase, IAzureService 21 | { 22 | public MicrosoftGraph(IAzureCredential credential) : base(credential) 23 | { 24 | } 25 | protected override string BuidEndPoint(string function, string optionalQuery) 26 | { 27 | var query = HttpUtility.ParseQueryString(optionalQuery); 28 | //query["api-version"] = "1.61-internal"; 29 | 30 | var builder = new UriBuilder("https://graph.microsoft.com/beta/" + function); 31 | builder.Query = query.ToString(); 32 | return builder.ToString(); 33 | } 34 | 35 | public GraphAPI.User GetMe() 36 | { 37 | return CallEndPoint("me"); 38 | } 39 | 40 | public List GetAuthorizationPolicy() 41 | { 42 | return CallEndPointWithPaggingAsync("policies/authorizationPolicy", null).GetAwaiter().GetResult(); 43 | } 44 | 45 | // message=Insufficient privileges to complete the operation. 46 | public string GetTenantRelationships(string tenantId) 47 | { 48 | return CallEndPoint("tenantRelationships/findTenantInformationByTenantId(tenantId='" + tenantId + "')"); 49 | } 50 | 51 | public class AuthorizationPolicy 52 | { 53 | public string id { get; set; } 54 | public string allowInvitesFrom { get; set; } 55 | public bool allowedToSignUpEmailBasedSubscriptions { get; set; } 56 | public bool allowedToUseSSPR { get; set; } 57 | public bool allowEmailVerifiedUsersToJoinOrganization { get; set; } 58 | public bool blockMsolPowerShell { get; set; } 59 | public string description { get; set; } 60 | public string displayName { get; set; } 61 | public List enabledPreviewFeatures { get; set; } 62 | public string guestUserRoleId { get; set; } 63 | public List permissionGrantPolicyIdsAssignedToDefaultUserRole { get; set; } 64 | public UserRolePermissions defaultUserRolePermissions { get; set; } 65 | } 66 | 67 | public class UserRolePermissions 68 | { 69 | public bool allowedToCreateApps { get; set; } 70 | public bool allowedToCreateSecurityGroups { get; set; } 71 | public bool allowedToCreateTenants { get; set; } 72 | public bool allowedToReadBitlockerKeysForOwnedDevice { get; set; } 73 | public bool allowedToReadOtherUsers { get; set; } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Template/TemplateManager.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Diagnostics; 10 | using System.IO; 11 | using System.IO.Compression; 12 | using System.Linq; 13 | using System.Reflection; 14 | using System.Text; 15 | using System.Threading.Tasks; 16 | 17 | namespace PingCastleCloud.Template 18 | { 19 | public class TemplateManager 20 | { 21 | private static string LoadTemplate(string resourceName) 22 | { 23 | var assembly = Assembly.GetExecutingAssembly(); 24 | Stream stream = null; 25 | GZipStream gzip = null; 26 | string html = null; 27 | StreamReader reader = null; 28 | try 29 | { 30 | stream = assembly.GetManifestResourceStream(resourceName); 31 | gzip = new GZipStream(stream, CompressionMode.Decompress); 32 | reader = new StreamReader(gzip); 33 | html = reader.ReadToEnd(); 34 | } 35 | catch (Exception) 36 | { 37 | Trace.WriteLine("Unable to load " + resourceName); 38 | throw; 39 | } 40 | finally 41 | { 42 | if (reader != null) 43 | reader.Dispose(); 44 | } 45 | return html; 46 | } 47 | 48 | public static string LoadResponsiveTemplate() 49 | { 50 | return LoadTemplate(typeof(TemplateManager).Namespace + ".responsivetemplate.html.gz"); 51 | } 52 | 53 | public static string LoadBootstrapCss() 54 | { 55 | return LoadTemplate(typeof(TemplateManager).Namespace + ".bootstrap.min.css.gz"); 56 | } 57 | 58 | public static string LoadBootstrapJs() 59 | { 60 | return LoadTemplate(typeof(TemplateManager).Namespace + ".bootstrap.min.js.gz"); 61 | } 62 | 63 | public static string LoadBootstrapTableCss() 64 | { 65 | return LoadTemplate(typeof(TemplateManager).Namespace + ".bootstrap-table.min.css.gz"); 66 | } 67 | 68 | public static string LoadBootstrapTableJs() 69 | { 70 | return LoadTemplate(typeof(TemplateManager).Namespace + ".bootstrap-table.min.js.gz"); 71 | } 72 | 73 | public static string LoadPopperJs() 74 | { 75 | return LoadTemplate(typeof(TemplateManager).Namespace + ".popper.min.js.gz"); 76 | } 77 | 78 | public static string LoadJqueryJs() 79 | { 80 | return LoadTemplate(typeof(TemplateManager).Namespace + ".jquery.min.js.gz"); 81 | } 82 | 83 | public static string LoadReportBaseCss() 84 | { 85 | return LoadTemplate(typeof(TemplateManager).Namespace + ".ReportBase.css.gz"); 86 | } 87 | 88 | public static string LoadReportBaseJs() 89 | { 90 | return LoadTemplate(typeof(TemplateManager).Namespace + ".ReportBase.js.gz"); 91 | } 92 | 93 | public static string LoadReportMainJs() 94 | { 95 | return LoadTemplate(typeof(TemplateManager).Namespace + ".ReportMain.js.gz"); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /RESTServices/O365/O365Api.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using Newtonsoft.Json; 8 | using PingCastleCloud.Credentials; 9 | using PingCastleCloud.RESTServices.Azure; 10 | using PingCastleCloud.Tokens; 11 | using System; 12 | using System.Collections.Generic; 13 | using System.Diagnostics; 14 | using System.Linq; 15 | using System.Text; 16 | using System.Threading.Tasks; 17 | using System.Web; 18 | 19 | namespace PingCastleCloud.RESTServices.O365 20 | { 21 | [AzureService("fb78d390-0c51-40cd-8e17-fdbfab77341b", "https://outlook.office365.com", Constants.OrganisationsNativeClient)] 22 | [EndPoint("https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize", "https://login.microsoftonline.com/organizations/oauth2/v2.0/token")] 23 | public class O365Api : RESTClientBase, IAzureService 24 | { 25 | public O365Api(IAzureCredential credential) : base(credential) 26 | { 27 | } 28 | 29 | protected override string BuidEndPoint(string function, string optionalQuery) 30 | { 31 | var query = HttpUtility.ParseQueryString(optionalQuery); 32 | 33 | var builder = new UriBuilder("https://outlook.office365.com/adminapi/beta/" + credential.Tenantid + "/" + function); 34 | builder.Query = query.ToString(); 35 | return builder.ToString(); 36 | } 37 | 38 | public List GetMailBoxes() 39 | { 40 | return CallEndPointWithPagging("MailBox", "?PropertySet=Minimum,Delivery"); 41 | } 42 | 43 | [DebuggerDisplay("{PrimarySmtpAddress} {DisplayName}")] 44 | public class MailBox 45 | { 46 | [JsonProperty("@odata.id")] 47 | public string OdataId { get; set; } 48 | 49 | [JsonProperty("@odata.editLink")] 50 | public string OdataEditLink { get; set; } 51 | public string ObjectKey { get; set; } 52 | public string ExternalDirectoryObjectId { get; set; } 53 | public bool MessageRecallProcessingEnabled { get; set; } 54 | public bool MessageCopyForSMTPClientSubmissionEnabled { get; set; } 55 | public bool MessageCopyForSentAsEnabled { get; set; } 56 | public bool MessageCopyForSendOnBehalfEnabled { get; set; } 57 | public bool DeliverToMailboxAndForward { get; set; } 58 | public bool MessageTrackingReadStatusEnabled { get; set; } 59 | public string ForwardingAddress { get; set; } 60 | public string ForwardingSmtpAddress { get; set; } 61 | public string DowngradeHighPriorityMessagesEnabled { get; set; } 62 | public string RecipientLimits { get; set; } 63 | public string RulesQuota { get; set; } 64 | public string UserPrincipalName { get; set; } 65 | public object MaxSafeSenders { get; set; } 66 | public object MaxBlockedSenders { get; set; } 67 | public List AcceptMessagesOnlyFrom { get; set; } 68 | public List AcceptMessagesOnlyFromDLMembers { get; set; } 69 | public List AcceptMessagesOnlyFromSendersOrMembers { get; set; } 70 | public string Alias { get; set; } 71 | public string DisplayName { get; set; } 72 | public List EmailAddresses { get; set; } 73 | public List GrantSendOnBehalfTo { get; set; } 74 | public string MaxSendSize { get; set; } 75 | public string MaxReceiveSize { get; set; } 76 | public string PrimarySmtpAddress { get; set; } 77 | public string RecipientType { get; set; } 78 | public string RecipientTypeDetails { get; set; } 79 | public List RejectMessagesFrom { get; set; } 80 | public List RejectMessagesFromDLMembers { get; set; } 81 | public List RejectMessagesFromSendersOrMembers { get; set; } 82 | public string Identity { get; set; } 83 | public string Id { get; set; } 84 | public string ExchangeVersion { get; set; } 85 | public string Name { get; set; } 86 | public string DistinguishedName { get; set; } 87 | public string OrganizationId { get; set; } 88 | public string Guid { get; set; } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /RESTServices/Azure/ManagementApi.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using Newtonsoft.Json; 8 | using PingCastleCloud.Credentials; 9 | using PingCastleCloud.Tokens; 10 | using System; 11 | using System.Collections.Generic; 12 | using System.Collections.Specialized; 13 | using System.Linq; 14 | using System.Net.Http; 15 | using System.Text; 16 | using System.Threading.Tasks; 17 | using System.Web; 18 | 19 | namespace PingCastleCloud.RESTServices.Azure 20 | { 21 | [AzureService("d3590ed6-52b3-4102-aeff-aad2292ab01c", "https://management.core.windows.net/")] 22 | public class ManagementApi : RESTClientBase, IAzureService 23 | { 24 | public ManagementApi(IAzureCredential credential) : base(credential) 25 | { 26 | } 27 | 28 | protected override string BuidEndPoint(string function, string optionalQuery) 29 | { 30 | var query = HttpUtility.ParseQueryString(optionalQuery); 31 | query["api-version"] = "2015-11-01"; 32 | 33 | var builder = new UriBuilder("https://management.azure.com/" + function); 34 | builder.Query = query.ToString(); 35 | return builder.ToString(); 36 | } 37 | 38 | public TenantListResponse ListTenants() 39 | { 40 | var r = new TenantListRequest 41 | { 42 | requests = new List() 43 | { 44 | new TenantListRequestItem { 45 | httpMethod = "GET", 46 | name = Guid.NewGuid().ToString(), 47 | url = "/tenants?api-version=2019-03-01&`$includeAllTenantCategories=true", 48 | requestHeaderDetails = new TenantListRequestHeaderDetails() 49 | { 50 | commandName = "fx.Services.Tenants.getTenants" 51 | } 52 | } 53 | } 54 | }; 55 | return CallEndPoint("batch", r); 56 | } 57 | 58 | public class TenantListRequestHeaderDetails 59 | { 60 | public string commandName { get; set; } 61 | } 62 | 63 | public class TenantListRequest 64 | { 65 | public List requests { get; set; } 66 | } 67 | 68 | public class TenantListRequestItem 69 | { 70 | public string httpMethod { get; set; } 71 | public string name { get; set; } 72 | public TenantListRequestHeaderDetails requestHeaderDetails { get; set; } 73 | public string url { get; set; } 74 | } 75 | 76 | public class Headers 77 | { 78 | public string Pragma { get; set; } 79 | 80 | [JsonProperty("x-ms-ratelimit-remaining-tenant-reads")] 81 | public string XMsRatelimitRemainingTenantReads { get; set; } 82 | 83 | [JsonProperty("x-ms-request-id")] 84 | public string XMsRequestId { get; set; } 85 | 86 | [JsonProperty("x-ms-correlation-request-id")] 87 | public string XMsCorrelationRequestId { get; set; } 88 | 89 | [JsonProperty("x-ms-routing-request-id")] 90 | public string XMsRoutingRequestId { get; set; } 91 | 92 | [JsonProperty("Strict-Transport-Security")] 93 | public string StrictTransportSecurity { get; set; } 94 | 95 | [JsonProperty("X-Content-Type-Options")] 96 | public string XContentTypeOptions { get; set; } 97 | 98 | [JsonProperty("Cache-Control")] 99 | public string CacheControl { get; set; } 100 | public string Date { get; set; } 101 | } 102 | 103 | public class Value 104 | { 105 | public string id { get; set; } 106 | public string tenantId { get; set; } 107 | public string countryCode { get; set; } 108 | public string displayName { get; set; } 109 | public List domains { get; set; } 110 | public string tenantCategory { get; set; } 111 | } 112 | 113 | public class Content 114 | { 115 | public List value { get; set; } 116 | } 117 | 118 | public class TenantListResponseItem 119 | { 120 | public string name { get; set; } 121 | public int httpStatusCode { get; set; } 122 | public Headers headers { get; set; } 123 | public Content content { get; set; } 124 | public int contentLength { get; set; } 125 | } 126 | 127 | public class TenantListResponse 128 | { 129 | public List responses { get; set; } 130 | } 131 | 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Template/ReportMain.js: -------------------------------------------------------------------------------- 1 | const applications = getData('App'); 2 | const dangerousAppPermissions = getData('dangerousAppPermissions'); 3 | const dangerousDelegatedPermissions = getData('dangerousDelegatedPermissions'); 4 | 5 | 6 | function dangerousAppPermission(value, row) { 7 | return dangerousAppPermissions.find(x => x == row.permission) != null; 8 | } 9 | function dangerousDelegatedPermission(value, row) { 10 | return dangerousDelegatedPermissions.find(x => x == row.permission) != null; 11 | } 12 | 13 | function delegatedPrincipal(value, row) { 14 | if (row.principalDisplayName) 15 | return row.principalDisplayName; 16 | return row.principalId; 17 | } 18 | 19 | function azureRole(value, row) { 20 | if (row.roleTemplateId) { 21 | return "" + encodeURI(row.displayName).replace("%20", " ") + ""; 23 | } 24 | return value; 25 | } 26 | //https://docs.microsoft.com/en-us/graph/permissions-reference 27 | function permissionFormatter(value, row) { 28 | var mark = ""; 29 | if (value.startsWith("User.")) { 30 | mark = "user-permissions"; 31 | } 32 | else if (value.startsWith("Group.")) { 33 | mark = "group-permissions"; 34 | } 35 | else if (value.startsWith("AccessReview.")) { 36 | mark = "access-reviews-permissions"; 37 | } 38 | else if (value.startsWith("AdministrativeUnit.")) { 39 | mark = "administrative-units-permissions"; 40 | } 41 | else if (value.startsWith("Analytics.")) { 42 | mark = "analytics-resource-permissions"; 43 | } 44 | else if (value.startsWith("AppCatalog.")) { 45 | mark = "appcatalog-resource-permissions"; 46 | } 47 | else if (value.startsWith("Application.")) { 48 | mark = "application-resource-permissions"; 49 | } 50 | else if (value.startsWith("AuditLog.")) { 51 | mark = "audit-log-permissions"; 52 | } 53 | else if (value.startsWith("BitlockerKey.")) { 54 | mark = "bitlocker-recovery-key-permissions"; 55 | } 56 | else if (value.startsWith("Calendars.")) { 57 | mark = "calendars-permissions"; 58 | } 59 | else if (value.startsWith("Contacts.")) { 60 | mark = "contacts-permissions"; 61 | } 62 | else if (value.startsWith("Directory.")) { 63 | mark = "directory-permissions"; 64 | } 65 | else if (value.startsWith("Domain.")) { 66 | mark = "domain-permissions"; 67 | } 68 | else if (value.startsWith("Files.")) { 69 | mark = "files-permissions"; 70 | } 71 | else if (value.startsWith("Group.")) { 72 | mark = "group-permissions"; 73 | } 74 | else if (value.startsWith("Mail.")) { 75 | mark = "mail-permissions"; 76 | } 77 | if (mark) { 78 | return "" + value + ""; 79 | } 80 | return value; 81 | } 82 | 83 | var ApplicationModal = new bootstrap.Modal(document.getElementById('application_modal'), { 84 | keyboard: false 85 | }); 86 | 87 | const container = document.querySelectorAll('.appcontainer'); 88 | 89 | container.forEach(x => x.addEventListener('click', function (e) { 90 | // But only alert for elements that have an alert-button class 91 | if (e.target.classList.contains('data-pc-toggle-app')) { 92 | e.preventDefault(); 93 | var appid = e.target.href.substring(e.target.href.indexOf("#") + 1); 94 | var app = applications.find(x => x.objectId == appid); 95 | var app_title = app.appDisplayName; 96 | if (!app_title) 97 | app_title = "AppID_" + app.appId; 98 | 99 | $('#application_modal .modal-header h4').text(app_title); 100 | 101 | $('#label_objectId').text(app.objectId); 102 | $('#label_appId').text(app.appId); 103 | $('#label_displayname').text(app_title); 104 | $('#label_tenantowner').text(app.appOwnerTenantId); 105 | 106 | $('#t_app_app').bootstrapTable('load', app.ApplicationPermissions); 107 | $('#t_app_delegated').bootstrapTable('load', app.DelegatedPermissions); 108 | $('#t_app_memberof').bootstrapTable('load', app.MemberOf); 109 | 110 | ApplicationModal.show(); 111 | } 112 | })); 113 | 114 | const foreignTenants = getData('ForeignTenants'); 115 | const containerForeignTenant = document.querySelectorAll('.foreigntenantcontainer'); 116 | var foreignTenantModal = new bootstrap.Modal(document.getElementById('tenant_modal'), { 117 | keyboard: false 118 | }); 119 | 120 | containerForeignTenant.forEach(x => x.addEventListener('click', function (e) { 121 | // But only alert for elements that have an alert-button class 122 | if (e.target.classList.contains('data-pc-toggle-tenant')) { 123 | e.preventDefault(); 124 | var tenantid = e.target.href.substring(e.target.href.indexOf("#") + 1); 125 | debugger; 126 | var tenants = foreignTenants.filter(x => x.TenantID == tenantid); 127 | tenants.sort(function (a, b) { return b.GuestsCount + b.MemberCount - a.GuestsCount - a.MemberCount }); 128 | //$('#tenant_modal .modal-header h4').text("Tenant:" + tenants[0].TenantID); 129 | 130 | $('#label_tenantId').text(tenants[0].TenantID); 131 | 132 | $('#t_tenant_domain').bootstrapTable('load', tenants); 133 | 134 | foreignTenantModal.show(); 135 | } 136 | })); 137 | 138 | function TotalDomains(value, row) { 139 | return row.GuestsCount + row.MemberCount; 140 | } -------------------------------------------------------------------------------- /Common/HttpClientHelper.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.Logs; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.IO; 11 | using System.Linq; 12 | using System.Net.Http; 13 | using System.ServiceModel.Channels; 14 | using System.Text; 15 | using System.Threading.Tasks; 16 | using System.Web; 17 | 18 | namespace PingCastleCloud.Common 19 | { 20 | public class HttpClientHelper 21 | { 22 | public static string BuildUri(string rootUri, IEnumerable> optionalQueryItems) 23 | { 24 | var builder = new UriBuilder(rootUri); 25 | builder.Port = -1; 26 | if (optionalQueryItems != null) 27 | { 28 | var query = HttpUtility.ParseQueryString(builder.Query); 29 | foreach (var item in optionalQueryItems) 30 | { 31 | query[item.Key] = item.Value; 32 | } 33 | builder.Query = query.ToString(); 34 | } 35 | return builder.ToString(); 36 | } 37 | 38 | public static void EnableLoging(SazGenerator generator) 39 | { 40 | if (_generator != null) 41 | { 42 | _generator.Dispose(); 43 | } 44 | _generator = generator; 45 | if (_cachedHttpClient != null) 46 | { 47 | _cachedHttpClient.Dispose(); 48 | _cachedHttpClient = null; 49 | } 50 | } 51 | static SazGenerator _generator; 52 | 53 | public static int LogNavigating(Uri url) 54 | { 55 | if (_generator == null) 56 | return 0; 57 | if (url.Scheme == "res") 58 | url = new Uri(url.ToString().Replace("res://", "http://")); 59 | if (url.Scheme != "http" && url.Scheme != "https") 60 | return 0; 61 | return _generator.RecordBeginQuery(new HttpRequestMessage(HttpMethod.Get, url)); 62 | } 63 | 64 | public static void LogNavigated(int logSessionId, Uri url, string documentText) 65 | { 66 | if (_generator == null) 67 | return; 68 | if (url.Scheme == "res") 69 | url = new Uri(url.ToString().Replace("res://", "http://")); 70 | if (url.Scheme != "http" && url.Scheme != "https") 71 | return; 72 | 73 | using (var ms = new MemoryStream()) 74 | { 75 | var b = Encoding.UTF8.GetBytes(documentText); 76 | ms.Write(b, 0, b.Length); 77 | ms.Position = 0; 78 | 79 | var result = new HttpResponseMessage(); 80 | result.Content = new StreamContent(ms); 81 | _generator.RecordEndQuery(logSessionId, result); 82 | } 83 | 84 | } 85 | 86 | public static int LogSoapBegin(Message request) 87 | { 88 | if (_generator == null) 89 | return 0; 90 | 91 | HttpRequestMessageProperty prop = null; 92 | if (request.Properties.ContainsKey("httpRequest")) 93 | { 94 | prop = (HttpRequestMessageProperty)request.Properties["httpRequest"]; 95 | } 96 | var m = HttpMethod.Get; 97 | if (prop != null && string.Equals(prop.Method, "post", StringComparison.OrdinalIgnoreCase)) 98 | m = HttpMethod.Post; 99 | var r = new HttpRequestMessage(m, request.Headers.Action); 100 | using (var ms = new MemoryStream()) 101 | { 102 | var b = Encoding.UTF8.GetBytes(request.ToString()); 103 | ms.Write(b, 0, b.Length); 104 | ms.Position = 0; 105 | 106 | r.Content = new StreamContent(ms); 107 | 108 | return _generator.RecordBeginQuery(r); 109 | } 110 | } 111 | 112 | public static void LogSoapEnd(int logSessionId, Message request) 113 | { 114 | if (_generator == null) 115 | return; 116 | 117 | using (var ms = new MemoryStream()) 118 | { 119 | var b = Encoding.UTF8.GetBytes(request.ToString()); 120 | ms.Write(b, 0, b.Length); 121 | ms.Position = 0; 122 | 123 | var result = new HttpResponseMessage(); 124 | result.Content = new StreamContent(ms); 125 | _generator.RecordEndQuery(logSessionId, result); 126 | } 127 | 128 | } 129 | 130 | public static string LogComment 131 | { 132 | get 133 | { 134 | if (_generator != null) 135 | return _generator.SessionComment; 136 | return null; 137 | } 138 | set 139 | { 140 | if (_generator != null) 141 | _generator.SessionComment = value; 142 | } 143 | } 144 | 145 | private static HttpClient _cachedHttpClient; 146 | public static HttpClient GetHttpClient() 147 | { 148 | if (_cachedHttpClient != null) 149 | return _cachedHttpClient; 150 | 151 | HttpMessageHandler handler = new HttpClientHandler() 152 | { 153 | AllowAutoRedirect = false 154 | }; 155 | 156 | if (_generator != null) 157 | { 158 | handler = new LoggingHandler(_generator, handler); 159 | } 160 | 161 | HttpClient client = new HttpClient(handler); 162 | client.DefaultRequestHeaders.Accept.Clear(); 163 | _cachedHttpClient = client; 164 | 165 | return _cachedHttpClient; 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | PingCastle.xlsx 2 | ResxManager-* 3 | ADWS/wsdl 4 | azure*.yml 5 | 6 | ## Ignore Visual Studio temporary files, build results, and 7 | ## files generated by popular Visual Studio add-ons. 8 | ## 9 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 10 | 11 | # User-specific files 12 | *.suo 13 | *.user 14 | *.userosscache 15 | *.sln.docstates 16 | 17 | # User-specific files (MonoDevelop/Xamarin Studio) 18 | *.userprefs 19 | 20 | # Build results 21 | [Dd]ebug/ 22 | [Dd]ebugPublic/ 23 | [Rr]elease/ 24 | [Rr]eleases/ 25 | x64/ 26 | x86/ 27 | bld/ 28 | [Bb]in/ 29 | [Oo]bj/ 30 | [Ll]og/ 31 | 32 | # Visual Studio 2015 cache/options directory 33 | .vs/ 34 | # Uncomment if you have tasks that create the project's static files in wwwroot 35 | #wwwroot/ 36 | 37 | # MSTest test Results 38 | [Tt]est[Rr]esult*/ 39 | [Bb]uild[Ll]og.* 40 | 41 | # NUNIT 42 | *.VisualState.xml 43 | TestResult.xml 44 | 45 | # Build Results of an ATL Project 46 | [Dd]ebugPS/ 47 | [Rr]eleasePS/ 48 | dlldata.c 49 | 50 | # .NET Core 51 | project.lock.json 52 | project.fragment.lock.json 53 | artifacts/ 54 | **/Properties/launchSettings.json 55 | 56 | *_i.c 57 | *_p.c 58 | *_i.h 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.svclog 79 | *.scc 80 | 81 | # Chutzpah Test files 82 | _Chutzpah* 83 | 84 | # Visual C++ cache files 85 | ipch/ 86 | *.aps 87 | *.ncb 88 | *.opendb 89 | *.opensdf 90 | *.sdf 91 | *.cachefile 92 | *.VC.db 93 | *.VC.VC.opendb 94 | 95 | # Visual Studio profiler 96 | *.psess 97 | *.vsp 98 | *.vspx 99 | *.sap 100 | 101 | # TFS 2012 Local Workspace 102 | $tf/ 103 | 104 | # Guidance Automation Toolkit 105 | *.gpState 106 | 107 | # ReSharper is a .NET coding add-in 108 | _ReSharper*/ 109 | *.[Rr]e[Ss]harper 110 | *.DotSettings.user 111 | 112 | # JustCode is a .NET coding add-in 113 | .JustCode 114 | 115 | # TeamCity is a build add-in 116 | _TeamCity* 117 | 118 | # DotCover is a Code Coverage Tool 119 | *.dotCover 120 | 121 | # Visual Studio code coverage results 122 | *.coverage 123 | *.coveragexml 124 | 125 | # NCrunch 126 | _NCrunch_* 127 | .*crunch*.local.xml 128 | nCrunchTemp_* 129 | 130 | # MightyMoose 131 | *.mm.* 132 | AutoTest.Net/ 133 | 134 | # Web workbench (sass) 135 | .sass-cache/ 136 | 137 | # Installshield output folder 138 | [Ee]xpress/ 139 | 140 | # DocProject is a documentation generator add-in 141 | DocProject/buildhelp/ 142 | DocProject/Help/*.HxT 143 | DocProject/Help/*.HxC 144 | DocProject/Help/*.hhc 145 | DocProject/Help/*.hhk 146 | DocProject/Help/*.hhp 147 | DocProject/Help/Html2 148 | DocProject/Help/html 149 | 150 | # Click-Once directory 151 | publish/ 152 | 153 | # Publish Web Output 154 | *.[Pp]ublish.xml 155 | *.azurePubxml 156 | # TODO: Comment the next line if you want to checkin your web deploy settings 157 | # but database connection strings (with potential passwords) will be unencrypted 158 | *.pubxml 159 | *.publishproj 160 | 161 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 162 | # checkin your Azure Web App publish settings, but sensitive information contained 163 | # in these scripts will be unencrypted 164 | PublishScripts/ 165 | 166 | # NuGet Packages 167 | *.nupkg 168 | # The packages folder can be ignored because of Package Restore 169 | **/packages/* 170 | # except build/, which is used as an MSBuild target. 171 | !**/packages/build/ 172 | # Uncomment if necessary however generally it will be regenerated when needed 173 | #!**/packages/repositories.config 174 | # NuGet v3's project.json files produces more ignorable files 175 | *.nuget.props 176 | *.nuget.targets 177 | 178 | # Microsoft Azure Build Output 179 | csx/ 180 | *.build.csdef 181 | 182 | # Microsoft Azure Emulator 183 | ecf/ 184 | rcf/ 185 | 186 | # Windows Store app package directories and files 187 | AppPackages/ 188 | BundleArtifacts/ 189 | Package.StoreAssociation.xml 190 | _pkginfo.txt 191 | 192 | # Visual Studio cache files 193 | # files ending in .cache can be ignored 194 | *.[Cc]ache 195 | # but keep track of directories ending in .cache 196 | !*.[Cc]ache/ 197 | 198 | # Others 199 | ClientBin/ 200 | ~$* 201 | *~ 202 | *.dbmdl 203 | *.dbproj.schemaview 204 | *.jfm 205 | *.pfx 206 | *.publishsettings 207 | orleans.codegen.cs 208 | 209 | # Since there are multiple workflows, uncomment next line to ignore bower_components 210 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 211 | #bower_components/ 212 | 213 | # RIA/Silverlight projects 214 | Generated_Code/ 215 | 216 | # Backup & report files from converting an old project file 217 | # to a newer Visual Studio version. Backup files are not needed, 218 | # because we have git ;-) 219 | _UpgradeReport_Files/ 220 | Backup*/ 221 | UpgradeLog*.XML 222 | UpgradeLog*.htm 223 | 224 | # SQL Server files 225 | *.mdf 226 | *.ldf 227 | 228 | # Business Intelligence projects 229 | *.rdl.data 230 | *.bim.layout 231 | *.bim_*.settings 232 | 233 | # Microsoft Fakes 234 | FakesAssemblies/ 235 | 236 | # GhostDoc plugin setting file 237 | *.GhostDoc.xml 238 | 239 | # Node.js Tools for Visual Studio 240 | .ntvs_analysis.dat 241 | node_modules/ 242 | 243 | # Typescript v1 declaration files 244 | typings/ 245 | 246 | # Visual Studio 6 build log 247 | *.plg 248 | 249 | # Visual Studio 6 workspace options file 250 | *.opt 251 | 252 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 253 | *.vbw 254 | 255 | # Visual Studio LightSwitch build output 256 | **/*.HTMLClient/GeneratedArtifacts 257 | **/*.DesktopClient/GeneratedArtifacts 258 | **/*.DesktopClient/ModelManifest.xml 259 | **/*.Server/GeneratedArtifacts 260 | **/*.Server/ModelManifest.xml 261 | _Pvt_Extensions 262 | 263 | # Paket dependency manager 264 | .paket/paket.exe 265 | paket-files/ 266 | 267 | # FAKE - F# Make 268 | .fake/ 269 | 270 | # JetBrains Rider 271 | .idea/ 272 | *.sln.iml 273 | 274 | # CodeRush 275 | .cr/ 276 | 277 | # Python Tools for Visual Studio (PTVS) 278 | __pycache__/ 279 | *.pyc 280 | 281 | # Cake - Uncomment if you are using it 282 | # tools/** 283 | # !tools/packages.config 284 | *.dll 285 | -------------------------------------------------------------------------------- /Template/ReportBase.css: -------------------------------------------------------------------------------- 1 | body { 2 | background:#e1e1e1; 3 | height: 100%; 4 | min-height: 100%; 5 | } 6 | html 7 | { 8 | height:100%; 9 | min-height: 100%; 10 | } 11 | @media screen { 12 | body { 13 | padding-top: 50px; 14 | margin-top: 10px; 15 | } 16 | 17 | h1, h2, h3, h4 { 18 | color: #Fa9C1A; 19 | } 20 | a { 21 | color: #fa9c1a; 22 | } 23 | 24 | a:hover { 25 | color: #58595b; 26 | } 27 | } 28 | #wrapper { 29 | box-shadow:0 0 8px rgba(0,0,0,0.25); 30 | } 31 | #wrapper.well { 32 | background:#fff; 33 | border-radius:0; 34 | } 35 | 36 | .ticked { color: #4CAF50;} 37 | 38 | .unticked { color: #FF1744;} 39 | 40 | 41 | .navbar-custom { 42 | background-color: #ffffff; 43 | } 44 | 45 | .navbar-custom .navbar-toggler-icon { 46 | background-image: url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E"); 47 | } 48 | .navbar-custom .navbar-toggler { 49 | color: rgba(0, 0, 0, 0.5); 50 | border-color: rgba(0, 0, 0, 0.1); 51 | } 52 | 53 | /* change the brand and text color */ 54 | .navbar-custom .navbar-brand, .navbar-custom .navbar-text { 55 | color: #fa9c1a; 56 | } 57 | /* change the link color */ 58 | .navbar-custom .navbar-nav .nav-link { 59 | color: #58595b; 60 | } 61 | 62 | .navbar-custom .navbar-nav > .active > a, 63 | .navbar-custom .navbar-nav > .active > a:hover, 64 | .navbar-custom .navbar-nav > .active > a:focus { 65 | color: #fff; 66 | background-color: #fa9c1a; 67 | } 68 | 69 | .nav-tabs .nav-link { 70 | border-color: #e9ecef #e9ecef #dee2e6; 71 | color: #58595b; 72 | } 73 | 74 | .nav-tabs .nav-link.disabled { 75 | color: #6c757d; 76 | background-color: transparent; 77 | border-color: transparent; 78 | } 79 | 80 | .nav-tabs .nav-link.active, 81 | .nav-tabs .nav-item.show .nav-link { 82 | color: #fff; 83 | background-color: #fa9c1a; 84 | border-color: #dee2e6 #dee2e6 #fff; 85 | } 86 | 87 | /* navbar */ 88 | .navbar-brand { 89 | margin: 0; 90 | padding: 0; 91 | } 92 | 93 | .navbar-brand img { 94 | max-height: 100%; 95 | } 96 | 97 | .dropdown-item.active, .dropdown-item:active { 98 | background-color: #fa9c1a; 99 | } 100 | 101 | .border-bottom { 102 | border-bottom-color: #fa9c1a !important; 103 | } 104 | 105 | .risk_model_none 106 | {background-color: #00AAFF; padding:5px;} 107 | .risk_model_low 108 | {background-color: #ffd800; padding:5px;} 109 | .risk_model_medium 110 | {background-color: #ff6a00; padding:5px;} 111 | .risk_model_high 112 | {background-color: #f12828; padding:5px;} 113 | 114 | .score_100 { background-color: #a856aa; padding: 5px;} 115 | .score_over75 { background-color: #f12828; padding: 5px;} 116 | .score_over50 { background-color: #ff6a00; padding: 5px;} 117 | .score_over25 { background-color: #ffd800; padding: 5px;} 118 | .score_below25 { background-color: #83e043; padding: 5px;} 119 | 120 | .modal-header{background-color: #FA9C1A;} 121 | .modal-header h4 {color: #fff;} 122 | 123 | /* no transition to be used in the carto mode */ 124 | .progress .progress-bar{ 125 | -webkit-transition: none; 126 | -o-transition: none; 127 | transition: none; 128 | } 129 | .info-mark { 130 | display: inline-block; 131 | font-family: sans-serif; 132 | font-weight: bold; 133 | text-align: center; 134 | width: 20px; 135 | height: 20px; 136 | font-size: 16px; 137 | line-height: 20px; 138 | border-radius: 14px; 139 | margin-right: 4px; 140 | padding: 1px; 141 | color: #fa9c1a; 142 | background: white; 143 | border: 1px solid #fa9c1a; 144 | text-decoration: none; 145 | } 146 | 147 | .info-mark:hover { 148 | color: white; 149 | background: #fa9c1a; 150 | border-color: white; 151 | text-decoration: none; 152 | } 153 | .num 154 | { 155 | text-align: right !important; 156 | } 157 | @media screen 158 | { 159 | a[name] { 160 | padding-top: 80px; 161 | margin-top: -80px; 162 | display: inline-block; /* required for webkit browsers */ 163 | } 164 | } 165 | .pagebreak { page-break-before: always; } /* page-break-after works, as well */ 166 | 167 | 168 | .btn-default { 169 | color: #58595B; 170 | background-color: transparent; 171 | background-image: none; 172 | border-color: #58595B; 173 | } 174 | 175 | .btn-default:hover { 176 | color: #fff; 177 | background-color: #58595B; 178 | border-color: #58595B; 179 | } 180 | 181 | .btn-default:focus, .btn-outline-primary.focus { 182 | box-shadow: 0 0 0 0.2rem rgba(88, 89, 91, 0.5); 183 | } 184 | 185 | .btn-default.disabled, .btn-outline-primary:disabled { 186 | color: #58595B; 187 | background-color: transparent; 188 | } 189 | 190 | .btn-default:not(:disabled):not(.disabled):active, .btn-outline-primary:not(:disabled):not(.disabled).active, 191 | .show > .btn-default.dropdown-toggle { 192 | color: #fff; 193 | background-color: #58595B; 194 | border-color: #58595B; 195 | } 196 | 197 | .btn-default:not(:disabled):not(.disabled):active:focus, .btn-default:not(:disabled):not(.disabled).active:focus, 198 | .show > .btn-default.dropdown-toggle:focus { 199 | box-shadow: 0 0 0 0.2rem rgba(88, 89, 91, 0.5); 200 | } 201 | 202 | .tooltip-wide { max-width: 100%; min-width: 80%; } 203 | 204 | .grade-1 { 205 | color:#fff; 206 | background-color:red; 207 | } 208 | .grade-2 { 209 | background-color:orange; 210 | color:#000; 211 | } 212 | .grade-3 { 213 | background-color:khaki; 214 | color:#000; 215 | } 216 | .grade-4 { 217 | color:#fff; 218 | background-color:#007bff 219 | } 220 | .grade-5 { 221 | color:#fff; 222 | background-color:#28a745 223 | } 224 | /* gauge */ 225 | .arc { 226 | } 227 | 228 | .chart-first { 229 | fill: #83e043; 230 | } 231 | 232 | .chart-second { 233 | fill: #ffd800; 234 | } 235 | 236 | .chart-third { 237 | fill: #ff6a00; 238 | } 239 | 240 | .chart-quart { 241 | fill: #f12828; 242 | } 243 | 244 | .needle, .needle-center { 245 | fill: #000000; 246 | } 247 | 248 | .text { 249 | color: #112864; 250 | } 251 | 252 | svg { 253 | font: 10px sans-serif; 254 | } 255 | 256 | .indicators-border { 257 | border: 2px solid #Fa9C1A; 258 | margin: 2px; 259 | padding: 2px; 260 | } 261 | 262 | path.type0 { 263 | fill: #e60049; 264 | } 265 | path.type1 { 266 | fill: #0bb4ff; 267 | } 268 | path.empty { 269 | fill: #f8f9fa; 270 | } 271 | g.sectors { 272 | stroke: white; 273 | stroke-width: 2px; 274 | } 275 | -------------------------------------------------------------------------------- /RESTServices/RESTClientBase.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using Newtonsoft.Json; 8 | using PingCastleCloud.Common; 9 | using PingCastleCloud.Credentials; 10 | using PingCastleCloud.Tokens; 11 | using System; 12 | using System.Collections.Generic; 13 | using System.Collections.Specialized; 14 | using System.Diagnostics; 15 | using System.Linq; 16 | using System.Net.Http; 17 | using System.Text; 18 | using System.Threading.Tasks; 19 | 20 | namespace PingCastleCloud.RESTServices.Azure 21 | { 22 | public abstract class RESTClientBase where API: IAzureService 23 | { 24 | protected IAzureCredential credential; 25 | protected RESTClientBase(IAzureCredential credential) 26 | { 27 | this.credential = credential; 28 | } 29 | 30 | protected U CallEndPoint(string function, T input) 31 | { 32 | return CallEndPointAsync(function, input).GetAwaiter().GetResult(); 33 | } 34 | 35 | protected U CallEndPoint(string function, string optionalQuery = "") 36 | { 37 | return CallEndPointAsync(function, null, optionalQuery).GetAwaiter().GetResult(); 38 | } 39 | 40 | public class Response : JsonSerialization> 41 | { 42 | [JsonProperty("odata.metadata")] 43 | public string OdataMetadata { get; set; } 44 | 45 | [JsonProperty("odata.nextLink")] 46 | public string OdataNextLink { get; set; } 47 | 48 | [JsonProperty("@odata.nextLink")] 49 | public string OdataNextLink2 { get { return OdataNextLink; } set { OdataNextLink = value; } } 50 | public List value { get; set; } 51 | } 52 | 53 | public class JsonErrorMessage 54 | { 55 | public string lang { get; set; } 56 | public string value { get; set; } 57 | } 58 | 59 | public class JsonErrorOdataError 60 | { 61 | public string code { get; set; } 62 | public JsonErrorMessage message { get; set; } 63 | public string requestId { get; set; } 64 | public DateTime date { get; set; } 65 | } 66 | 67 | public class JsonError : JsonSerialization 68 | { 69 | [JsonProperty("odata.error")] 70 | public JsonErrorOdataError OdataError { get; set; } 71 | } 72 | 73 | 74 | public List CallEndPointWithPagging(string function, T input = default(T), string optionalQuery = "") 75 | { 76 | return CallEndPointWithPaggingAsync(function, input, optionalQuery).GetAwaiter().GetResult(); 77 | } 78 | 79 | public List CallEndPointWithPagging(string function,string optionalQuery = "") 80 | { 81 | return CallEndPointWithPaggingAsync(function, null, optionalQuery).GetAwaiter().GetResult(); 82 | } 83 | 84 | public async Task> CallEndPointWithPaggingAsync(string function, T input, string optionalQuery = "") 85 | { 86 | 87 | var r = await CallEndPointAsync>(function, input, optionalQuery); 88 | var output = new List(); 89 | output.AddRange(r.value); 90 | 91 | while (!string.IsNullOrEmpty(r.OdataNextLink)) 92 | { 93 | var builder = new UriBuilder(r.OdataNextLink); 94 | r = await CallEndPointAsync>(function, input, builder.Query); 95 | output.AddRange(r.value); 96 | } 97 | 98 | return output; 99 | } 100 | 101 | public async Task CallEndPointWithPaggingAndActionAsync(string function, T input, Action action, string optionalQuery = "") 102 | { 103 | 104 | var r = await CallEndPointAsync>(function, input, optionalQuery); 105 | foreach(var a in r.value) 106 | { 107 | action(a); 108 | } 109 | while (!string.IsNullOrEmpty(r.OdataNextLink)) 110 | { 111 | var builder = new UriBuilder(r.OdataNextLink); 112 | r = await CallEndPointAsync>(function, input, builder.Query); 113 | foreach (var a in r.value) 114 | { 115 | action(a); 116 | } 117 | } 118 | } 119 | 120 | protected async Task CallEndPointAsync(string function, T input, string optionalQuery = "") 121 | { 122 | var token = await credential.GetToken(); 123 | 124 | var httpClient = HttpClientHelper.GetHttpClient(); 125 | var requestUri = BuidEndPoint(function, optionalQuery); 126 | Trace.WriteLine("Calling " + requestUri); 127 | using (var request = new HttpRequestMessage(input == null ? HttpMethod.Get : HttpMethod.Post, requestUri)) 128 | { 129 | request.Headers.Add("Authorization", "Bearer " + token.access_token); 130 | request.Headers.Add("x-ms-client-request-id", Guid.NewGuid().ToString()); 131 | if (input != null) 132 | { 133 | var jsonInput = JsonConvert.SerializeObject(input); 134 | request.Content = new StringContent(jsonInput, Encoding.UTF8, "application/json"); 135 | } 136 | 137 | var response = await httpClient.SendAsync(request); 138 | if (!response.IsSuccessStatusCode) 139 | { 140 | Trace.WriteLine("ErrorCode: " + response.StatusCode); 141 | var jsonError = await response.Content.ReadAsStringAsync(); 142 | Trace.WriteLine("Error: " + jsonError); 143 | JsonError error = null; 144 | try 145 | { 146 | error = JsonConvert.DeserializeObject(jsonError); 147 | } 148 | catch 149 | { 150 | 151 | } 152 | if (error != null && error.OdataError != null && error.OdataError.message != null) 153 | { 154 | throw new ApplicationException("Error when calling " + requestUri + " : " + error.OdataError.message.value); 155 | } 156 | // default error handling 157 | response.EnsureSuccessStatusCode(); 158 | } 159 | 160 | var jsonOuput = await response.Content.ReadAsStringAsync(); 161 | return JsonConvert.DeserializeObject(jsonOuput); 162 | } 163 | } 164 | 165 | 166 | virtual protected string BuidEndPoint(string function, string optionalQuery) 167 | { 168 | throw new NotImplementedException(); 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /Rules/CustomRulesSettings.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using System; 8 | using System.Configuration; 9 | 10 | namespace PingCastleCloud.Rules 11 | { 12 | internal class CustomRulesSettings : ConfigurationSection 13 | { 14 | static CustomRulesSettings cachedSettings = null; 15 | public static CustomRulesSettings GetCustomRulesSettings() 16 | { 17 | if (cachedSettings == null) 18 | cachedSettings = ConfigurationManager.GetSection("customRulesSettings") as CustomRulesSettings; 19 | return cachedSettings; 20 | } 21 | 22 | [ConfigurationProperty("CustomRules", IsRequired = false)] 23 | public CustomRulesCollection CustomRules 24 | { 25 | get 26 | { 27 | return base["CustomRules"] as CustomRulesCollection; 28 | } 29 | } 30 | } 31 | 32 | [ConfigurationCollection(typeof(CustomRuleSettings), AddItemName = "CustomRule")] 33 | internal class CustomRulesCollection : ConfigurationElementCollection 34 | { 35 | public CustomRulesCollection() 36 | { 37 | 38 | } 39 | 40 | public CustomRuleSettings this[int index] 41 | { 42 | get { return (CustomRuleSettings)BaseGet(index); } 43 | set 44 | { 45 | if (BaseGet(index) != null) 46 | { 47 | BaseRemoveAt(index); 48 | } 49 | BaseAdd(index, value); 50 | } 51 | } 52 | 53 | public void Add(CustomRuleSettings pluginConfig) 54 | { 55 | BaseAdd(pluginConfig); 56 | } 57 | 58 | public void Clear() 59 | { 60 | BaseClear(); 61 | } 62 | 63 | protected override ConfigurationElement CreateNewElement() 64 | { 65 | return new CustomRuleSettings(); 66 | } 67 | 68 | protected override object GetElementKey(ConfigurationElement element) 69 | { 70 | return ((CustomRuleSettings)element).RiskId; 71 | } 72 | 73 | public void Remove(CustomRuleSettings pluginConfig) 74 | { 75 | BaseRemove(pluginConfig.RiskId); 76 | } 77 | 78 | public void RemoveAt(int index) 79 | { 80 | BaseRemoveAt(index); 81 | } 82 | 83 | public void Remove(string name) 84 | { 85 | BaseRemove(name); 86 | } 87 | 88 | } 89 | 90 | public class CustomRuleSettings : ConfigurationElement 91 | { 92 | [ConfigurationProperty("RiskId", IsKey = true, IsRequired = true)] 93 | public string RiskId 94 | { 95 | get 96 | { 97 | return base["RiskId"] as string; 98 | } 99 | set 100 | { 101 | base["RiskId"] = value; 102 | } 103 | } 104 | 105 | [ConfigurationProperty("Computations")] 106 | public ComputationCollection Computations 107 | { 108 | get 109 | { 110 | return (ComputationCollection)base["Computations"]; 111 | } 112 | } 113 | 114 | [ConfigurationProperty("MaturityLevel")] 115 | public int MaturityLevel 116 | { 117 | get 118 | { 119 | return (int) base["MaturityLevel"]; 120 | } 121 | set 122 | { 123 | base["MaturityLevel"] = value; 124 | } 125 | } 126 | } 127 | 128 | [ConfigurationCollection(typeof(CustomRuleComputationSettings), AddItemName = "Computation", CollectionType = ConfigurationElementCollectionType.BasicMap)] 129 | public class ComputationCollection : ConfigurationElementCollection 130 | { 131 | public CustomRuleComputationSettings this[int index] 132 | { 133 | get { return (CustomRuleComputationSettings)BaseGet(index); } 134 | set 135 | { 136 | if (BaseGet(index) != null) 137 | { 138 | BaseRemoveAt(index); 139 | } 140 | BaseAdd(index, value); 141 | } 142 | } 143 | 144 | public void Add(CustomRuleComputationSettings serviceConfig) 145 | { 146 | BaseAdd(serviceConfig); 147 | } 148 | 149 | public void Clear() 150 | { 151 | BaseClear(); 152 | } 153 | 154 | protected override ConfigurationElement CreateNewElement() 155 | { 156 | return new CustomRuleComputationSettings(); 157 | } 158 | 159 | protected override object GetElementKey(ConfigurationElement element) 160 | { 161 | return ((CustomRuleComputationSettings)element).Order; 162 | } 163 | 164 | public void Remove(CustomRuleComputationSettings serviceConfig) 165 | { 166 | BaseRemove(serviceConfig.Order); 167 | } 168 | 169 | public void RemoveAt(int index) 170 | { 171 | BaseRemoveAt(index); 172 | } 173 | 174 | public void Remove(String name) 175 | { 176 | BaseRemove(name); 177 | } 178 | 179 | } 180 | 181 | public class CustomRuleComputationSettings : ConfigurationElement 182 | { 183 | [ConfigurationProperty("Type", IsRequired = true)] 184 | public RuleComputationType Type 185 | { 186 | get 187 | { 188 | return (RuleComputationType) base["Type"]; 189 | } 190 | set 191 | { 192 | base["Type"] = value; 193 | } 194 | } 195 | 196 | [ConfigurationProperty("Score", IsRequired = true)] 197 | public int Score 198 | { 199 | get 200 | { 201 | return (int)base["Score"]; 202 | } 203 | set 204 | { 205 | base["Score"] = value; 206 | } 207 | } 208 | 209 | [ConfigurationProperty("Order", IsRequired = false, DefaultValue=1)] 210 | public int Order 211 | { 212 | get 213 | { 214 | return (int)base["Order"]; 215 | } 216 | set 217 | { 218 | base["Order"] = value; 219 | } 220 | } 221 | 222 | [ConfigurationProperty("Threshold", IsRequired = false)] 223 | public int Threshold 224 | { 225 | get 226 | { 227 | return (int)base["Threshold"]; 228 | } 229 | set 230 | { 231 | base["Threshold"] = value; 232 | } 233 | } 234 | 235 | public RuleComputationAttribute GetAttribute() 236 | { 237 | return new RuleComputationAttribute(Type, Score, Threshold, Order); 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /Rules/RuleBase.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.Data; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Resources; 12 | using System.Text; 13 | using System.Threading.Tasks; 14 | 15 | namespace PingCastleCloud.Rules 16 | { 17 | public abstract class RuleBase 18 | { 19 | public string RiskId { get; set; } 20 | public string Title { get; private set; } 21 | public string Description { get; private set; } 22 | public string TechnicalExplanation { get; private set; } 23 | public string Solution { get; private set; } 24 | public string Documentation { get; private set; } 25 | private string DetailRationale; 26 | protected string DetailFormatString; 27 | // used to provide a location of the data in the report (aka a link to the report) 28 | public string ReportLocation { get; private set; } 29 | 30 | public int Points { get; set; } 31 | public string Rationale { get; set; } 32 | 33 | public List Details { get; set; } 34 | 35 | public int MaturityLevel { get; set; } 36 | 37 | // return count if not null or rely on details.count 38 | protected virtual int? AnalyzeDataNew(HealthCheckCloudData healthcheckData) 39 | { 40 | throw new NotImplementedException(); 41 | } 42 | 43 | public List RuleComputation { get; private set; } 44 | 45 | ResourceManager _resourceManager; 46 | protected ResourceManager ResourceManager 47 | { 48 | get 49 | { 50 | if (_resourceManager == null) 51 | { 52 | _resourceManager = new ResourceManager(GetType().Namespace + ".RuleDescription", GetType().Assembly); 53 | } 54 | return _resourceManager; 55 | } 56 | } 57 | 58 | protected RuleBase() 59 | { 60 | object[] models = GetType().GetCustomAttributes(typeof(RuleModelAttribute), true); 61 | if (models != null && models.Length != 0) 62 | { 63 | RuleModelAttribute model = (RuleModelAttribute)models[0]; 64 | RiskId = model.Id; 65 | } 66 | else 67 | { 68 | throw new NotImplementedException(); 69 | } 70 | string resourceKey; 71 | resourceKey = RiskId.Replace('-', '_').Replace('$', '_'); 72 | 73 | Title = ResourceManager.GetString(resourceKey + "_Title"); 74 | Description = ResourceManager.GetString(resourceKey + "_Description"); 75 | TechnicalExplanation = ResourceManager.GetString(resourceKey + "_TechnicalExplanation"); 76 | Solution = ResourceManager.GetString(resourceKey + "_Solution"); 77 | Documentation = ResourceManager.GetString(resourceKey + "_Documentation"); 78 | DetailRationale = ResourceManager.GetString(resourceKey + "_Rationale"); 79 | 80 | DetailFormatString = ResourceManager.GetString(resourceKey + "_Detail"); 81 | ReportLocation = ResourceManager.GetString(resourceKey + "_ReportLocation"); 82 | 83 | RuleComputation = new List((RuleComputationAttribute[])GetType().GetCustomAttributes(typeof(RuleComputationAttribute), true)); 84 | if (RuleComputation.Count == 0) 85 | throw new NotImplementedException(); 86 | RuleComputation.Sort((RuleComputationAttribute a, RuleComputationAttribute b) 87 | => 88 | { 89 | return a.Order.CompareTo(b.Order); 90 | } 91 | ); 92 | if (!String.IsNullOrEmpty(Documentation)) 93 | { 94 | string[] lines = Documentation.Split( 95 | new[] { "\r\n", "\r", "\n" }, 96 | StringSplitOptions.None 97 | ); 98 | for (int i = 0; i < lines.Length; i++) 99 | { 100 | lines[i] = "" + lines[i] + ""; 101 | } 102 | Documentation = string.Join("
\r\n", lines); 103 | } 104 | object[] frameworks = GetType().GetCustomAttributes(typeof(RuleFrameworkReference), true); 105 | if (frameworks != null && frameworks.Length != 0) 106 | { 107 | if (!String.IsNullOrEmpty(Documentation)) 108 | Documentation += "
\r\n"; 109 | for (int i = 0; i < frameworks.Length; i++) 110 | { 111 | if (i > 0) 112 | Documentation += "
\r\n"; 113 | Documentation += ((RuleFrameworkReference)frameworks[i]).GenerateLink(); 114 | } 115 | } 116 | 117 | var ruleMaturity = new List((IRuleMaturity[])GetType().GetCustomAttributes(typeof(IRuleMaturity), true)); 118 | if (ruleMaturity.Count == 0) 119 | { 120 | MaturityLevel = 0; 121 | } 122 | MaturityLevel = 6; 123 | foreach (var m in ruleMaturity) 124 | { 125 | if (MaturityLevel > m.Level) 126 | MaturityLevel = m.Level; 127 | } 128 | } 129 | 130 | public void Initialize() 131 | { 132 | Details = new List(); 133 | } 134 | 135 | public void AddDetail(string detail) 136 | { 137 | Details.Add(detail); 138 | } 139 | 140 | public void AddRawDetail(params object[] data) 141 | { 142 | AddDetail(String.Format(DetailFormatString, data)); 143 | } 144 | 145 | public bool Analyze(HealthCheckCloudData healthcheckData) 146 | { 147 | bool hasTheRuleMatched = false; 148 | // PingCastle 2.5 149 | Points = 0; 150 | int? valueReturnedByAnalysis = AnalyzeDataNew(healthcheckData); 151 | if (valueReturnedByAnalysis == null) 152 | { 153 | valueReturnedByAnalysis = 0; 154 | if (Details != null) 155 | valueReturnedByAnalysis = Details.Count; 156 | } 157 | foreach (var computation in RuleComputation) 158 | { 159 | int points = 0; 160 | if (computation.HasMatch((int)valueReturnedByAnalysis, ref points)) 161 | { 162 | hasTheRuleMatched = true; 163 | Points = points; 164 | UpdateLabelsAfterMatch((int)valueReturnedByAnalysis, computation); 165 | break; 166 | } 167 | } 168 | return hasTheRuleMatched; 169 | } 170 | 171 | protected virtual void UpdateLabelsAfterMatch(int valueReturnedByAnalysis, RuleComputationAttribute computation) 172 | { 173 | if (DetailRationale != null) 174 | { 175 | Rationale = DetailRationale; 176 | Rationale = Rationale.Replace("{count}", valueReturnedByAnalysis.ToString()); 177 | Rationale = Rationale.Replace("{threshold}", computation.Threshold.ToString()); 178 | } 179 | } 180 | 181 | public string GetComputationModelString() 182 | { 183 | return RuleComputationAttribute.GetComputationModelString(RuleComputation); 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /Rules/RuleSet.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Diagnostics; 10 | using System.Reflection; 11 | using PingCastleCloud.Data; 12 | 13 | namespace PingCastleCloud.Rules 14 | { 15 | public class RuleSet 16 | { 17 | private static Dictionary _cachedRules = null; 18 | 19 | public static IEnumerable Rules 20 | { 21 | get 22 | { 23 | if (_cachedRules == null || _cachedRules.Count == 0) 24 | { 25 | ReloadRules(); 26 | } 27 | return _cachedRules.Values; 28 | } 29 | } 30 | 31 | public static void ReloadRules() 32 | { 33 | _cachedRules = new Dictionary(); 34 | LoadRules(_cachedRules); 35 | } 36 | 37 | public static void LoadRules(Dictionary rules) 38 | { 39 | // important: to work with W2000, we cannot use GetType because it will instanciate .Net 3.0 class then load the missing assembly 40 | // the trick here is to check only the exported type and put as internal the class using .Net 3.0 functionalities 41 | foreach (Type type in Assembly.GetAssembly(typeof(RuleSet)).GetExportedTypes()) 42 | { 43 | if (type.IsSubclassOf(typeof(RuleBase)) && !type.IsAbstract) 44 | { 45 | try 46 | { 47 | var a = (RuleBase)Activator.CreateInstance(type); 48 | rules.Add(a.RiskId, a); 49 | } 50 | catch (Exception) 51 | { 52 | Trace.WriteLine("Unable to instanciate the type " + type); 53 | throw; 54 | } 55 | } 56 | } 57 | } 58 | 59 | public static void LoadCustomRules() 60 | { 61 | // force the load of rules 62 | var output = Rules; 63 | 64 | var customRules = CustomRulesSettings.GetCustomRulesSettings(); 65 | if (customRules.CustomRules != null) 66 | { 67 | foreach (CustomRuleSettings rule in customRules.CustomRules) 68 | { 69 | var riskId = rule.RiskId; 70 | RuleBase matchedRule = GetRuleFromID(riskId); 71 | if (matchedRule == null) 72 | { 73 | Trace.WriteLine("Rule computation does not match an existing ID (" + riskId + ")"); 74 | continue; 75 | } 76 | if (rule.Computations != null) 77 | { 78 | matchedRule.RuleComputation.Clear(); 79 | foreach (CustomRuleComputationSettings c in rule.Computations) 80 | { 81 | matchedRule.RuleComputation.Add(c.GetAttribute()); 82 | } 83 | } 84 | if (rule.MaturityLevel != 0) 85 | { 86 | matchedRule.MaturityLevel = rule.MaturityLevel; 87 | } 88 | } 89 | } 90 | } 91 | 92 | // when multiple reports are ran each after each other, internal state can be kept 93 | void ReInitRule(RuleBase rule) 94 | { 95 | rule.Initialize(); 96 | } 97 | 98 | public List ComputeRiskRules(HealthCheckCloudData data) 99 | { 100 | var output = new List(); 101 | Trace.WriteLine("Begining to run risk rule"); 102 | foreach (var rule in Rules) 103 | { 104 | string ruleName = rule.GetType().ToString(); 105 | Trace.WriteLine("Rule: " + ruleName); 106 | try 107 | { 108 | ReInitRule(rule); 109 | if (rule.Analyze(data)) 110 | { 111 | Trace.WriteLine(" matched"); 112 | output.Add(rule); 113 | } 114 | } 115 | catch (Exception ex) 116 | { 117 | Console.ForegroundColor = ConsoleColor.Red; 118 | Console.WriteLine("An exception occured when running the rule : " + ruleName); 119 | Trace.WriteLine("An exception occured when running the rule : " + ruleName); 120 | Console.WriteLine("Please contact support@pingcastle.com with the following details so the problem can be fixed"); 121 | Console.ResetColor(); 122 | Console.WriteLine("Message: " + ex.Message); 123 | Trace.WriteLine("Message: " + ex.Message); 124 | Console.WriteLine("StackTrace: " + ex.StackTrace); 125 | Trace.WriteLine("StackTrace: " + ex.StackTrace); 126 | if (ex.InnerException != null) 127 | { 128 | Console.WriteLine("Inner StackTrace: " + ex.InnerException.StackTrace); 129 | Trace.WriteLine("Inner StackTrace: " + ex.InnerException.StackTrace); 130 | } 131 | } 132 | 133 | } 134 | Trace.WriteLine("Risk rule run stopped"); 135 | ReComputeTotals(data, output.ConvertAll(x => x)); 136 | return output; 137 | } 138 | 139 | public static void ReComputeTotals(HealthCheckCloudData data, IEnumerable rules) 140 | { 141 | // consolidate scores 142 | data.GlobalScore = 0; 143 | /*data.StaleObjectsScore = 0; 144 | data.PrivilegiedGroupScore = 0; 145 | data.TrustScore = 0; 146 | data.AnomalyScore = 0;*/ 147 | data.MaturityLevel = 5; 148 | foreach (var rule in rules) 149 | { 150 | /*switch (rule.Category) 151 | { 152 | case RiskRuleCategory.Anomalies: 153 | data.AnomalyScore += rule.Points; 154 | break; 155 | case RiskRuleCategory.PrivilegedAccounts: 156 | data.PrivilegiedGroupScore += rule.Points; 157 | break; 158 | case RiskRuleCategory.StaleObjects: 159 | data.StaleObjectsScore += rule.Points; 160 | break; 161 | case RiskRuleCategory.Trusts: 162 | data.TrustScore += rule.Points; 163 | break; 164 | }*/ 165 | data.GlobalScore += rule.Points; 166 | var hcrule = RuleSet.GetRuleFromID(rule.RiskId); 167 | if (hcrule != null) 168 | { 169 | int level = hcrule.MaturityLevel; 170 | if (level > 0 && level < data.MaturityLevel) 171 | data.MaturityLevel = level; 172 | } 173 | } 174 | // limit to 100 175 | /*if (data.StaleObjectsScore > 100) 176 | data.StaleObjectsScore = 100; 177 | if (data.PrivilegiedGroupScore > 100) 178 | data.PrivilegiedGroupScore = 100; 179 | if (data.TrustScore > 100) 180 | data.TrustScore = 100; 181 | if (data.AnomalyScore > 100) 182 | data.AnomalyScore = 100; 183 | // max of all scores 184 | data.GlobalScore = Math.Max(data.StaleObjectsScore, 185 | Math.Max(data.PrivilegiedGroupScore, 186 | Math.Max(data.TrustScore, data.AnomalyScore)));*/ 187 | } 188 | 189 | public static string GetRuleDescription(string ruleid) 190 | { 191 | if (_cachedRules == null || _cachedRules.Count == 0) 192 | { 193 | ReloadRules(); 194 | } 195 | if (_cachedRules.ContainsKey(ruleid)) 196 | return _cachedRules[ruleid].Title; 197 | return String.Empty; 198 | } 199 | 200 | public static RuleBase GetRuleFromID(string ruleid) 201 | { 202 | if (_cachedRules == null || _cachedRules.Count == 0) 203 | { 204 | ReloadRules(); 205 | } 206 | if (_cachedRules.ContainsKey(ruleid)) 207 | return _cachedRules[ruleid]; 208 | return null; 209 | 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /PingCastleCloud.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {138C3C69-1245-4BBA-87B5-075E7140B826} 8 | Exe 9 | Properties 10 | PingCastleCloud 11 | PingCastleCloud 12 | v4.7.2 13 | 512 14 | 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | pingcastle.ico 37 | 38 | 39 | 40 | packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | Form 121 | 122 | 123 | AuthenticationDialog.cs 124 | 125 | 126 | 127 | 128 | Designer 129 | 130 | 131 | AuthenticationDialog.cs 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | cd $(ProjectDir)\template 159 | powershell.exe -ExecutionPolicy Bypass -NoProfile -NonInteractive -File "ProcessTemplate.ps1" 160 | 161 | 162 | SET PATH=%25PATH%25;"C:\Program Files (x86)\Windows Kits\8.0\bin\x86" 163 | SET SIGN_SHA256=signtool.exe sign /d "PingCastle Cloud" /tr http://timestamp.digicert.com /td sha256 /fd sha256 /n "Ping Castle SAS" /a 164 | SET SIGN_SHA256_DEBUG=signtool.exe sign /d "PingCastle Cloud" /td sha256 /fd sha256 /n "Ping Castle SAS" /a 165 | @echo ================ 166 | @echo signature 167 | @echo ================ 168 | IF /I "$(COMPUTERNAME)" == "DESKTOP-QCK4J75" ( 169 | IF /I "$(ConfigurationName)" == "Release" ( 170 | 171 | %25SIGN_SHA256%25 "$(TargetPath)" 172 | ) 173 | IF /I "$(ConfigurationName)" == "Debug" ( 174 | 175 | %25SIGN_SHA256_DEBUG%25 "$(TargetPath)" 176 | ) 177 | ) 178 | 179 | 186 | -------------------------------------------------------------------------------- /Rules/RuleDescription.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // Ce code a été généré par un outil. 4 | // Version du runtime :4.0.30319.42000 5 | // 6 | // Les modifications apportées à ce fichier peuvent provoquer un comportement incorrect et seront perdues si 7 | // le code est régénéré. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace PingCastleCloud.Rules { 12 | using System; 13 | 14 | 15 | /// 16 | /// Une classe de ressource fortement typée destinée, entre autres, à la consultation des chaînes localisées. 17 | /// 18 | // Cette classe a été générée automatiquement par la classe StronglyTypedResourceBuilder 19 | // à l'aide d'un outil, tel que ResGen ou Visual Studio. 20 | // Pour ajouter ou supprimer un membre, modifiez votre fichier .ResX, puis réexécutez ResGen 21 | // avec l'option /str ou régénérez votre projet VS. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class RuleDescription { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal RuleDescription() { 33 | } 34 | 35 | /// 36 | /// Retourne l'instance ResourceManager mise en cache utilisée par cette classe. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PingCastleCloud.Rules.RuleDescription", typeof(RuleDescription).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Remplace la propriété CurrentUICulture du thread actuel pour toutes 51 | /// les recherches de ressources à l'aide de cette classe de ressource fortement typée. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Recherche une chaîne localisée semblable à The purpose of this rule is to check that ADConnect has no know vulnerabilities. 65 | /// 66 | internal static string ADConnectVersion_Description { 67 | get { 68 | return ResourceManager.GetString("ADConnectVersion_Description", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Recherche une chaîne localisée semblable à Version: {0}. 74 | /// 75 | internal static string ADConnectVersion_Detail { 76 | get { 77 | return ResourceManager.GetString("ADConnectVersion_Detail", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Recherche une chaîne localisée semblable à https://dirteam.com/sander/2021/08/10/two-new-azure-ad-connect-versions-were-released-to-prevent-mitm-attacks-towards-domain-controllers-cve-2021-36949/ 83 | ///https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-36949. 84 | /// 85 | internal static string ADConnectVersion_Documentation { 86 | get { 87 | return ResourceManager.GetString("ADConnectVersion_Documentation", resourceCulture); 88 | } 89 | } 90 | 91 | /// 92 | /// Recherche une chaîne localisée semblable à The ADConnect version is known to be vulnerable. 93 | /// 94 | internal static string ADConnectVersion_Rationale { 95 | get { 96 | return ResourceManager.GetString("ADConnectVersion_Rationale", resourceCulture); 97 | } 98 | } 99 | 100 | /// 101 | /// Recherche une chaîne localisée semblable à . 102 | /// 103 | internal static string ADConnectVersion_ReportLocation { 104 | get { 105 | return ResourceManager.GetString("ADConnectVersion_ReportLocation", resourceCulture); 106 | } 107 | } 108 | 109 | /// 110 | /// Recherche une chaîne localisée semblable à You need to update ADConnect. 111 | /// . 112 | /// 113 | internal static string ADConnectVersion_Solution { 114 | get { 115 | return ResourceManager.GetString("ADConnectVersion_Solution", resourceCulture); 116 | } 117 | } 118 | 119 | /// 120 | /// Recherche une chaîne localisée semblable à Azure AD Connect v1.6.11.3 is is security update release of Azure AD Connect v1.x. This release addresses the vulnerability as documented in CVE-2021-36949. 121 | /// Azure AD Connect v2.0.8.0 is a security update release of Azure AD Connect v2.x. This release addresses the vulnerability as documented in CVE-2021-36949.. 122 | /// 123 | internal static string ADConnectVersion_TechnicalExplanation { 124 | get { 125 | return ResourceManager.GetString("ADConnectVersion_TechnicalExplanation", resourceCulture); 126 | } 127 | } 128 | 129 | /// 130 | /// Recherche une chaîne localisée semblable à Check if ADConnect is known to be vulnerable. 131 | /// 132 | internal static string ADConnectVersion_Title { 133 | get { 134 | return ResourceManager.GetString("ADConnectVersion_Title", resourceCulture); 135 | } 136 | } 137 | 138 | /// 139 | /// Recherche une chaîne localisée semblable à The purpose of this rule is to check that ADConnect is not running version 1. 140 | /// 141 | internal static string ADConnectVersion1_Description { 142 | get { 143 | return ResourceManager.GetString("ADConnectVersion1_Description", resourceCulture); 144 | } 145 | } 146 | 147 | /// 148 | /// Recherche une chaîne localisée semblable à Version: {0}. 149 | /// 150 | internal static string ADConnectVersion1_Detail { 151 | get { 152 | return ResourceManager.GetString("ADConnectVersion1_Detail", resourceCulture); 153 | } 154 | } 155 | 156 | /// 157 | /// Recherche une chaîne localisée semblable à https://azure.microsoft.com/en-us/updates/action-required-upgrade-to-the-latest-version-of-azure-ad-connect-before-31-august-2022/ 158 | ///https://docs.microsoft.com/en-us/azure/active-directory/hybrid/how-to-upgrade-previous-version. 159 | /// 160 | internal static string ADConnectVersion1_Documentation { 161 | get { 162 | return ResourceManager.GetString("ADConnectVersion1_Documentation", resourceCulture); 163 | } 164 | } 165 | 166 | /// 167 | /// Recherche une chaîne localisée semblable à The ADConnect version is on the branch 1.X.X.X. 168 | /// 169 | internal static string ADConnectVersion1_Rationale { 170 | get { 171 | return ResourceManager.GetString("ADConnectVersion1_Rationale", resourceCulture); 172 | } 173 | } 174 | 175 | /// 176 | /// Recherche une chaîne localisée semblable à . 177 | /// 178 | internal static string ADConnectVersion1_ReportLocation { 179 | get { 180 | return ResourceManager.GetString("ADConnectVersion1_ReportLocation", resourceCulture); 181 | } 182 | } 183 | 184 | /// 185 | /// Recherche une chaîne localisée semblable à You need to update ADConnect. 186 | /// . 187 | /// 188 | internal static string ADConnectVersion1_Solution { 189 | get { 190 | return ResourceManager.GetString("ADConnectVersion1_Solution", resourceCulture); 191 | } 192 | } 193 | 194 | /// 195 | /// Recherche une chaîne localisée semblable à Azure AD Connect v1.X.X.X is obsolete starting August, 31th 2022. It is recommended to use version 2.X.X.X.. 196 | /// 197 | internal static string ADConnectVersion1_TechnicalExplanation { 198 | get { 199 | return ResourceManager.GetString("ADConnectVersion1_TechnicalExplanation", resourceCulture); 200 | } 201 | } 202 | 203 | /// 204 | /// Recherche une chaîne localisée semblable à Check if ADConnect is on the branch 1.X.X.X. 205 | /// 206 | internal static string ADConnectVersion1_Title { 207 | get { 208 | return ResourceManager.GetString("ADConnectVersion1_Title", resourceCulture); 209 | } 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /Export/ExportAsGuest.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.Common; 8 | using PingCastleCloud.Credentials; 9 | using PingCastleCloud.RESTServices; 10 | using PingCastleCloud.RESTServices.Azure; 11 | using PingCastleCloud.Tokens; 12 | using System; 13 | using System.Collections.Generic; 14 | using System.IO; 15 | using System.Linq; 16 | using System.Text; 17 | using System.Threading; 18 | using System.Threading.Tasks; 19 | 20 | namespace PingCastleCloud.Export 21 | { 22 | public class ExportAsGuest 23 | { 24 | private IAzureCredential credential; 25 | private string tenantId; 26 | const int MaxParallel = 20; 27 | public ExportAsGuest(IAzureCredential credential) 28 | { 29 | this.credential = credential; 30 | if (string.IsNullOrEmpty(credential.TenantidToQuery)) 31 | throw new ApplicationException("No tenantId provided"); 32 | this.tenantId = credential.TenantidToQuery; 33 | } 34 | public void Export(string init) 35 | { 36 | HttpClientHelper.LogComment = "Export Guests"; 37 | var knownObjects = new SynchronizedCollection(); 38 | 39 | var groupToAnalyse = new SynchronizedCollection(); 40 | var userToAnalyse = new SynchronizedCollection(); 41 | var roleToAnalyse = new SynchronizedCollection(); 42 | var groups = new Dictionary(); 43 | var g = new GraphAPI(credential); 44 | 45 | 46 | 47 | using (var swuser = TextWriter.Synchronized(File.CreateText(tenantId + "_users.txt"))) 48 | using (var swgroup = TextWriter.Synchronized(File.CreateText(tenantId + "_groups.txt"))) 49 | using (var swgroupmember = TextWriter.Synchronized(File.CreateText(tenantId + "_groups_membership.txt"))) 50 | using (var swrole = TextWriter.Synchronized(File.CreateText(tenantId + "_roles.txt"))) 51 | using (var swrolemember = TextWriter.Synchronized(File.CreateText(tenantId + "_roles_membership.txt"))) 52 | using (var swerrors = TextWriter.Synchronized(File.CreateText(tenantId + "_errors.txt"))) 53 | using (var swadministrativeunit = TextWriter.Synchronized(File.CreateText(tenantId + "_administrativeunits.txt"))) 54 | { 55 | swuser.WriteLine("objectId,userType,userprincipalname,displayname"); 56 | swgroup.WriteLine("objectId,displayname"); 57 | swgroupmember.WriteLine("groupId,userId"); 58 | swrole.WriteLine("objectId,displayname"); 59 | swrolemember.WriteLine("roleId,userId"); 60 | swerrors.WriteLine("objectId,message"); 61 | swadministrativeunit.WriteLine("objectId"); 62 | 63 | GraphAPI.User u; 64 | var usersInput = init.Split(',', '\n').ToList(); 65 | Console.WriteLine(usersInput.Count + " user(s) to proceed"); 66 | foreach (var t in usersInput) 67 | { 68 | try 69 | { 70 | u = g.GetUser(t.Trim()); 71 | swuser.WriteLine(u.objectId + "," + u.userType + "," + u.userPrincipalName + "," + u.displayName); 72 | userToAnalyse.Add(u.objectId); 73 | } 74 | catch (Exception ex) 75 | { 76 | Console.WriteLine("Unable to locate " + t.Trim() + "(" + ex.Message + ")"); 77 | } 78 | } 79 | if (userToAnalyse.Count == 0) 80 | { 81 | Console.WriteLine("No user found to start the analyze"); 82 | return; 83 | } 84 | 85 | int iteration = 1; 86 | while (userToAnalyse.Count > 0 || groupToAnalyse.Count > 0) 87 | { 88 | HttpClientHelper.LogComment = "Export Guests Iteration " + iteration; 89 | Console.WriteLine("Iteration " + iteration++); 90 | Console.WriteLine("Processing users"); 91 | int userLoopCount = 1; 92 | Console.WriteLine(userToAnalyse.Count + " user(s) to analyze"); 93 | Parallel.ForEach(userToAnalyse, new ParallelOptions { MaxDegreeOfParallelism = MaxParallel }, (user) => 94 | { 95 | var count = Interlocked.Increment(ref userLoopCount); 96 | if ((count % 1000) == 0) 97 | { 98 | Console.WriteLine("Analyzed " + count + " users. " + (userToAnalyse.Count - count) + " to go."); 99 | } 100 | if (knownObjects.Contains(user)) 101 | return; 102 | 103 | knownObjects.Add(user); 104 | 105 | try 106 | { 107 | var membership = g.GetUserMembership(user); 108 | foreach (var m in membership) 109 | { 110 | if (m.objectType == "Role") 111 | { 112 | swrolemember.WriteLine(m.objectId + "," + user); 113 | if (knownObjects.Contains(m.objectId)) 114 | continue; 115 | if (roleToAnalyse.Contains(m.objectId)) 116 | continue; 117 | Console.WriteLine("Found role " + m.displayName); 118 | roleToAnalyse.Add(m.objectId); 119 | swrole.WriteLine(m.objectId + "," + m.displayName); 120 | } 121 | else if (m.objectType == "Group") 122 | { 123 | if (knownObjects.Contains(m.objectId)) 124 | continue; 125 | if (groupToAnalyse.Contains(m.objectId)) 126 | continue; 127 | Console.WriteLine("Found group " + m.displayName); 128 | swgroup.WriteLine(m.objectId + "," + m.displayName); 129 | groups[m.objectId] = m.displayName; 130 | groupToAnalyse.Add(m.objectId); 131 | } 132 | else if (m.objectType == "AdministrativeUnit") 133 | { 134 | if (knownObjects.Contains(m.objectId)) 135 | continue; 136 | swadministrativeunit.WriteLine(m.objectId); 137 | knownObjects.Add(m.objectId); 138 | } 139 | else 140 | { 141 | Console.WriteLine("Unknown membership type " + m.objectType); 142 | } 143 | } 144 | } 145 | catch (Exception ex) 146 | { 147 | swerrors.WriteLine(user + "," + ex.Message); 148 | } 149 | }); 150 | userToAnalyse.Clear(); 151 | 152 | Console.WriteLine("Processing groups"); 153 | Parallel.ForEach(groupToAnalyse, new ParallelOptions { MaxDegreeOfParallelism = MaxParallel }, 154 | (group) => 155 | { 156 | if (knownObjects.Contains(group)) 157 | return; 158 | try 159 | { 160 | knownObjects.Add(group); 161 | int users = 0; 162 | g.GetGroupMembers(group, (m) => 163 | { 164 | swgroupmember.WriteLine(group + "," + m.objectId); 165 | 166 | if (knownObjects.Contains(m.objectId)) 167 | return; 168 | if (userToAnalyse.Contains(m.objectId)) 169 | return; 170 | swuser.WriteLine(m.objectId + "," + m.userType + "," + m.userPrincipalName + "," + m.displayName); 171 | // usertype may be empty (member, guest). Avoid to analyze these users. 172 | if (!string.IsNullOrEmpty(m.userType)) 173 | userToAnalyse.Add(m.objectId); 174 | users++; 175 | if ((users % 1000) == 0) 176 | { 177 | Console.WriteLine("Busy enumerating group " + groups[group] + " (currently " + users + " users)"); 178 | } 179 | 180 | }); 181 | if (users > 0) 182 | Console.WriteLine("Found " + users + " user(s) in " + groups[group]); 183 | } 184 | catch (Exception ex) 185 | { 186 | swerrors.WriteLine(group + "," + ex.Message); 187 | } 188 | }); 189 | groupToAnalyse.Clear(); 190 | Console.WriteLine("Done"); 191 | } 192 | 193 | } 194 | HttpClientHelper.LogComment = ""; 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /License.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using System; 8 | using System.ComponentModel; 9 | using System.Configuration; 10 | using System.Diagnostics; 11 | using System.IO; 12 | using System.IO.Compression; 13 | using System.Security.Cryptography; 14 | using System.Text; 15 | 16 | namespace PingCastleCloud 17 | { 18 | 19 | public interface IPingCastleLicenseInfo 20 | { 21 | string GetSerialNumber(); 22 | } 23 | 24 | public class PingCastleLicenseProvider : LicenseProvider 25 | { 26 | 27 | #region Public Methods 28 | 29 | /// 30 | /// Gets a license for an instance or type of component. 31 | /// 32 | /// A that specifies where you can use the licensed object. 33 | /// A that represents the component requesting the license. 34 | /// An object that is requesting the license. 35 | /// true if a should be thrown when the component cannot be granted a license; otherwise, false. 36 | /// A valid . 37 | public override License GetLicense(LicenseContext context, Type type, object instance, bool allowExceptions) 38 | { 39 | IPingCastleLicenseInfo licenseInfo = (IPingCastleLicenseInfo)instance; 40 | return new PingCastleLicense(licenseInfo.GetSerialNumber()); 41 | } 42 | #endregion 43 | 44 | } 45 | 46 | internal class PingCastleLicenseSettings : ConfigurationSection 47 | { 48 | private static PingCastleLicenseSettings settings; 49 | 50 | public static PingCastleLicenseSettings Settings 51 | { 52 | get 53 | { 54 | if (settings == null) 55 | settings = ConfigurationManager.GetSection("LicenseSettings") as PingCastleLicenseSettings; 56 | return settings; 57 | } 58 | } 59 | 60 | [ConfigurationProperty("license", IsRequired = false)] 61 | public string License 62 | { 63 | get { return (string)this["license"]; } 64 | set { this["license"] = value; } 65 | } 66 | } 67 | 68 | public class PingCastleLicense : License, IDisposable 69 | { 70 | private bool _disposed = false; 71 | private string _licKey = null; 72 | 73 | public PingCastleLicense(string license) 74 | : this(license, true) 75 | { 76 | 77 | } 78 | 79 | public PingCastleLicense(string license, bool DoAKeyCheck) 80 | { 81 | if (String.IsNullOrEmpty(license)) 82 | throw new PingCastleCloudException("No PingCastle license has been provided"); 83 | _licKey = license; 84 | Trace.WriteLine("License: " + _licKey); 85 | if (!VerifyKey()) 86 | { 87 | throw new PingCastleCloudException("The PingCastle license is not valid"); 88 | } 89 | } 90 | 91 | #region Properties 92 | 93 | public DateTime EndTime { get; set; } 94 | public string DomainLimitation { get; set; } 95 | public string CustomerNotice { get; set; } 96 | public string Edition { get; set; } 97 | public int? DomainNumberLimit { get; set; } 98 | 99 | /// 100 | /// Gets the license key granted to this component. 101 | /// 102 | public override string LicenseKey 103 | { 104 | get { return _licKey; } 105 | } 106 | 107 | #endregion 108 | 109 | private bool VerifyKey() 110 | { 111 | #if DEBUG 112 | if (_licKey.Equals("debug", StringComparison.InvariantCultureIgnoreCase)) 113 | { 114 | EndTime = DateTime.MaxValue; 115 | DomainLimitation = null; 116 | CustomerNotice = "debug version"; 117 | return true; 118 | } 119 | #endif 120 | try 121 | { 122 | Trace.WriteLine("starting the license analysis"); 123 | 124 | Trace.WriteLine("License info uncompressed"); 125 | if (_licKey != null && _licKey.StartsWith("PC2")) 126 | { 127 | VerifyLicenseV2(); 128 | } 129 | else 130 | { 131 | VerifyLicenseV1(); 132 | } 133 | return true; 134 | } 135 | catch (Exception ex) 136 | { 137 | Trace.Write("License: exception " + ex.Message); 138 | return false; 139 | } 140 | } 141 | 142 | private void VerifyLicenseV2() 143 | { 144 | byte[] b = Convert.FromBase64String(_licKey.Substring(3)); 145 | using (MemoryStream ms = new MemoryStream(b)) 146 | { 147 | using (GZipStream gs = new GZipStream(ms, CompressionMode.Decompress)) 148 | { 149 | using (var ms2 = new MemoryStream()) 150 | { 151 | while (true) 152 | { 153 | int infoType = readint(gs); 154 | int infoLength = readint(gs); 155 | byte[] data = new byte[infoLength]; 156 | gs.Read(data, 0, data.Length); 157 | Trace.WriteLine("data Type = " + infoType); 158 | switch (infoType) 159 | { 160 | case 0: 161 | Trace.WriteLine("Signature"); 162 | VerifySignature(data, ms2.ToArray()); 163 | if (Edition == "Pro" && DomainNumberLimit == null) 164 | DomainNumberLimit = 1; 165 | return; 166 | case 1: 167 | Trace.WriteLine("EndTime"); 168 | EndTime = DateTime.FromFileTimeUtc(BitConverter.ToInt64(data, 0)); 169 | break; 170 | case 2: 171 | Trace.WriteLine("DomainLimitation"); 172 | DomainLimitation = Encoding.Unicode.GetString(data); 173 | break; 174 | case 3: 175 | Trace.WriteLine("CustomerNotice"); 176 | CustomerNotice = Encoding.Unicode.GetString(data); 177 | break; 178 | case 4: 179 | Trace.WriteLine("Edition"); 180 | Edition = Encoding.Unicode.GetString(data); 181 | break; 182 | case 5: 183 | DomainNumberLimit = BitConverter.ToInt32(data, 0); 184 | break; 185 | } 186 | ms2.Write(BitConverter.GetBytes(infoType), 0, 4); 187 | ms2.Write(BitConverter.GetBytes(data.Length), 0, 4); 188 | ms2.Write(data, 0, data.Length); 189 | } 190 | } 191 | } 192 | } 193 | 194 | } 195 | 196 | private void VerifyLicenseV1() 197 | { 198 | byte[] b = Convert.FromBase64String(_licKey); 199 | 200 | MemoryStream ms = new MemoryStream(); 201 | ms.Write(b, 0, b.Length); 202 | ms.Position = 0; 203 | byte[] date = new byte[readint(ms)]; 204 | byte[] limitation = new byte[readint(ms)]; 205 | byte[] notice = new byte[readint(ms)]; 206 | byte[] signature = new byte[readint(ms)]; 207 | Trace.WriteLine("reading date"); 208 | ms.Read(date, 0, date.Length); 209 | Trace.WriteLine("reading limitation"); 210 | ms.Read(limitation, 0, limitation.Length); 211 | Trace.WriteLine("reading notice"); 212 | ms.Read(notice, 0, notice.Length); 213 | Trace.WriteLine("reading signature"); 214 | ms.Read(signature, 0, signature.Length); 215 | Trace.WriteLine("reading done"); 216 | byte[] bytes = new byte[date.Length + limitation.Length + notice.Length]; 217 | 218 | Array.Copy(date, 0, bytes, 0, date.Length); 219 | Array.Copy(limitation, 0, bytes, date.Length, limitation.Length); 220 | Array.Copy(notice, 0, bytes, limitation.Length + date.Length, notice.Length); 221 | 222 | VerifySignature(signature, bytes); 223 | 224 | EndTime = DateTime.FromFileTimeUtc(BitConverter.ToInt64(date, 0)); 225 | Trace.WriteLine("Endtime=" + EndTime); 226 | DomainLimitation = Encoding.Unicode.GetString(limitation); 227 | Trace.WriteLine("DomainLimitation=" + DomainLimitation); 228 | CustomerNotice = Encoding.Unicode.GetString(notice); 229 | Trace.WriteLine("CustomerNotice=" + CustomerNotice); 230 | Trace.WriteLine("license verified"); 231 | } 232 | 233 | private void VerifySignature(byte[] signature, byte[] dataToVerify) 234 | { 235 | Trace.WriteLine("hashing license info"); 236 | using (SHA1 hashstring = SHA1.Create()) 237 | { 238 | byte[] hash = hashstring.ComputeHash(dataToVerify); 239 | Trace.WriteLine("hashing done"); 240 | Trace.WriteLine("loading rsa key"); 241 | using (RSACryptoServiceProvider RSA = LoadRSAKey()) 242 | { 243 | Trace.WriteLine("loading rsa key"); 244 | Trace.WriteLine("verifying the signature"); 245 | if (!RSA.VerifyHash(hash, "1.3.14.3.2.26", signature)) 246 | { 247 | throw new Exception("Invalid signature"); 248 | } 249 | Trace.WriteLine("signature ok"); 250 | } 251 | } 252 | } 253 | 254 | private RSACryptoServiceProvider LoadRSAKey() 255 | { 256 | RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(); 257 | RSAParameters parameters = new RSAParameters(); 258 | parameters.Modulus = Convert.FromBase64String("wNtlwFv+zo0lrShHnSi5VLT6Sbfx3ZXhtefSJfYs3YjWyPHv3ihLjXlBjMlGI5ziXrjcriNNZ5zn2P2qvv3VdX02zsIuGuAYZi0c4WBhiqtKgTo7USxsAaGxpqiWTkW3NQylw27p3jqICO7cbLXsr3aEZJJUgqkNay/l4S3pYIs="); 259 | parameters.Exponent = Convert.FromBase64String("AQAB"); 260 | RSA.ImportParameters(parameters); 261 | return RSA; 262 | } 263 | 264 | int readint(Stream stream) 265 | { 266 | byte[] temp = new byte[4]; 267 | stream.Read(temp, 0, 4); 268 | int size = BitConverter.ToInt32(temp, 0); 269 | return size; 270 | } 271 | 272 | /// 273 | /// Disposes this object. 274 | /// 275 | public sealed override void Dispose() 276 | { 277 | Dispose(true); 278 | GC.SuppressFinalize(this); 279 | } 280 | 281 | /// 282 | /// Disposes this object. 283 | /// 284 | /// true if the object is disposing. 285 | protected virtual void Dispose(bool disposing) 286 | { 287 | if (disposing) 288 | { 289 | if (!_disposed) 290 | { 291 | //Custom disposing here. 292 | } 293 | _disposed = true; 294 | } 295 | } 296 | 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /Data/HealthCheckCloudData.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using PingCastleCloud.Common; 8 | using PingCastleCloud.RESTServices.Azure; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Diagnostics; 12 | using System.Linq; 13 | using System.Runtime.Serialization; 14 | using System.Text; 15 | using System.Threading.Tasks; 16 | 17 | namespace PingCastleCloud.Data 18 | { 19 | [DebuggerDisplay("{appDisplayName}")] 20 | public class HealthCheckCloudDataForwardingMailboxes 21 | { 22 | public string ForwardingSmtpAddress { get; set; } 23 | public string PrimarySmtpAddress { get; set; } 24 | } 25 | 26 | [DebuggerDisplay("{appDisplayName}")] 27 | public class HealthCheckCloudDataApplication 28 | { 29 | public string appId { get; set; } 30 | public string appDisplayName { get; set; } 31 | public string appOwnerTenantId { get; set; } 32 | public List DelegatedPermissions { get; set; } 33 | public List ApplicationPermissions { get; set; } 34 | public List MemberOf { get; set; } 35 | public string objectId { get; set; } 36 | } 37 | 38 | [DebuggerDisplay("{permission} {resourceId} {consentType} {principalId}")] 39 | public class HealthCheckCloudDataApplicationOAuth2PermissionGrant 40 | { 41 | public string resourceId { get; set; } 42 | public string permission { get; set; } 43 | public string consentType { get; set; } 44 | public string principalId { get; set; } 45 | public string principalDisplayName { get; set; } 46 | } 47 | 48 | [DebuggerDisplay("{permission} {resourceId} {principalType} {resourceDisplayName}")] 49 | public class HealthCheckCloudDataApplicationRoleAssignedTo 50 | { 51 | public string permissionId { get; set; } 52 | 53 | public string resourceDisplayName { get; set; } 54 | 55 | public string resourceId { get; set; } 56 | public string permission { get; set; } 57 | public string principalType { get; set; } 58 | } 59 | [DebuggerDisplay("{displayName} {roleTemplateId}")] 60 | public class HealthCheckCloudDataApplicationMemberOf 61 | { 62 | public string displayName { get; set; } 63 | public string roleTemplateId { get; set; } 64 | } 65 | 66 | [DebuggerDisplay("{ObjectId} {UserPrincipalName}")] 67 | public class HealthCheckCloudDataUser 68 | { 69 | public Guid? ObjectId { get; set; } 70 | 71 | public string UserPrincipalName { get; set; } 72 | 73 | public DateTime? WhenCreated { get; set; } 74 | 75 | public DateTime? LastPasswordChangeTimestamp { get; set; } 76 | 77 | public bool? PasswordNeverExpires { get; set; } 78 | 79 | public bool HasImmutableId { get; set; } 80 | } 81 | 82 | [DebuggerDisplay("{Name} {IsInitial} {VerificationMethod}")] 83 | public class HealthCheckCloudDataDomain 84 | { 85 | public string Authentication { get; set; } 86 | 87 | public string Capabilities { get; set; } 88 | 89 | public bool IsDefault { get; set; } 90 | 91 | public bool IsInitial { get; set; } 92 | 93 | public string Name { get; set; } 94 | 95 | public string RootDomain { get; set; } 96 | 97 | public string Status { get; set; } 98 | 99 | public string VerificationMethod { get; set; } 100 | } 101 | 102 | [DebuggerDisplay("{Domain} {TenantID} {Region}")] 103 | public class HealthCheckCloudDataForeignDomains 104 | { 105 | 106 | public string Domain { get; set; } 107 | 108 | public int GuestsCount { get; set; } 109 | 110 | public string Region { get; set; } 111 | 112 | public string TenantID { get; set; } 113 | 114 | public int MemberCount { get; set; } 115 | } 116 | 117 | [DebuggerDisplay("{Name} {Type} {Definition}")] 118 | public class HealthCheckCloudDataNetworkPolicy 119 | { 120 | 121 | public string Name { get; set; } 122 | 123 | public string Type { get; set; } 124 | 125 | public string Definition { get; set; } 126 | 127 | public bool Trusted { get; set; } 128 | public bool ApplyToUnknownCountry { get; set; } 129 | } 130 | 131 | [DebuggerDisplay("{TenantId}")] 132 | public class HealthCheckCloudDataCrossTenantPolicy 133 | { 134 | 135 | public string TenantId { get; set; } 136 | 137 | public string lastModified { get; set; } 138 | 139 | public bool? AllowB2BFrom { get; set; } 140 | 141 | public bool? AllowNativeFederationFrom { get; set; } 142 | public bool? AllowB2BTo { get; set; } 143 | 144 | public bool? AllowNativeFederationTo { get; set; } 145 | 146 | } 147 | 148 | [DebuggerDisplay("{TenantID} {Name} {CountryCode}")] 149 | public class HealthCheckCloudDataTenantInformation 150 | { 151 | public string TenantID { get; set; } 152 | 153 | public string TenantCategory { get; set; } 154 | 155 | public string Name { get; set; } 156 | 157 | public string CountryCode { get; set; } 158 | 159 | public List Domains { get; set; } 160 | } 161 | 162 | [DebuggerDisplay("{ObjectId} {EmailAddress} {DisplayName}")] 163 | public class HealthCheckCloudDataRoleMember 164 | { 165 | 166 | 167 | public Guid? ObjectId { get; set; } 168 | 169 | public string EmailAddress { get; set; } 170 | 171 | public string DisplayName { get; set; } 172 | 173 | public DateTime? LastDirSyncTime { get; set; } 174 | 175 | public bool? IsLicensed { get; set; } 176 | 177 | public string OverallProvisioningStatus { get; set; } 178 | 179 | public string RoleMemberType { get; set; } 180 | 181 | public string ValidationStatus { get; set; } 182 | 183 | public bool? PasswordNeverExpires { get; set; } 184 | 185 | public DateTime? LastPasswordChangeTimestamp { get; set; } 186 | 187 | public DateTime? WhenCreated { get; set; } 188 | 189 | public bool HasImmutableId { get; set; } 190 | public List MFAStatus { get; set; } 191 | } 192 | 193 | [DebuggerDisplay("{ObjectId} {Name} {Description}")] 194 | public class HealthCheckCloudDataRole 195 | { 196 | 197 | public Guid? ObjectId { get; set; } 198 | 199 | public bool? IsSystem { get; set; } 200 | 201 | public string Name { get; set; } 202 | 203 | public bool? IsEnabled { get; set; } 204 | 205 | public string Description { get; set; } 206 | 207 | public int NumMembers { get; set; } 208 | public List members { get; set; } 209 | public int NumNoMFA { get; set; } 210 | } 211 | 212 | [DebuggerDisplay("{RiskId} {Rationale}")] 213 | public class HealthCheckCloudDataRiskRule 214 | { 215 | public int Points { get; set; } 216 | 217 | // we are using a xml serialization trick to be resilient if a new RiskRuleCategory is added in the future 218 | public string RiskId { get; set; } 219 | 220 | public string Rationale { get; set; } 221 | 222 | public List Details { get; set; } 223 | } 224 | public class HealthCheckCloudData : JsonSerialization 225 | { 226 | 227 | public DateTime GenerationDate { get; set; } 228 | 229 | public string EngineVersion { get; set; } 230 | 231 | 232 | public string TenantName { get; set; } 233 | 234 | public string TenantId { get; set; } 235 | 236 | public DateTime TenantCreation { get; set; } 237 | 238 | 239 | public string Region { get; set; } 240 | // from JWT : onprem_sid 241 | 242 | public string DomainSID { get; set; } 243 | 244 | public string ProvisionDisplayName { get; set; } 245 | 246 | public string ProvisionStreet { get; set; } 247 | 248 | public string ProvisionCity { get; set; } 249 | 250 | public string ProvisionPostalCode { get; set; } 251 | 252 | public string ProvisionCountry { get; set; } 253 | 254 | public string ProvisionState { get; set; } 255 | 256 | public string ProvisionTelephoneNumber { get; set; } 257 | 258 | public string ProvisionCountryLetterCode { get; set; } 259 | 260 | public string ProvisionInitialDomain { get; set; } 261 | 262 | public DateTime? ProvisionLastDirSyncTime { get; set; } 263 | 264 | public DateTime? ProvisionLastPasswordSyncTime { get; set; } 265 | 266 | public bool ProvisionSelfServePasswordResetEnabled { get; set; } 267 | 268 | public List ProvisionTechnicalNotificationEmails { get; set; } 269 | 270 | public List ProvisionMarketingNotificationEmails { get; set; } 271 | 272 | public List ProvisionSecurityComplianceNotificationEmails { get; set; } 273 | 274 | public string ProvisionDirSyncApplicationType { get; set; } 275 | 276 | public string ProvisionDirSyncClientMachineName { get; set; } 277 | 278 | public string ProvisionDirSyncClientVersion { get; set; } 279 | 280 | public string ProvisionDirSyncServiceAccount { get; set; } 281 | 282 | public string ProvisionDirectorySynchronizationStatus { get; set; } 283 | 284 | public List ProvisionCompanyTags { get; set; } 285 | 286 | public string ProvisionCompanyType { get; set; } 287 | 288 | public bool? ProvisionPasswordSynchronizationEnabled { get; set; } 289 | 290 | public List ProvisionAuthorizedServiceInstances { get; set; } 291 | 292 | 293 | public List Domains { get; set; } 294 | 295 | public List Roles { get; set; } 296 | 297 | public int NumberOfUsers { get; set; } 298 | public List UsersInactive { get; set; } 299 | public List UsersPasswordNeverExpires { get; set; } 300 | 301 | public List ForeignDomains { get; set; } 302 | public List CrossTenantPolicies { get; set; } 303 | public List NetworkPolicies { get; set; } 304 | 305 | public int NumberofGuests { get; set; } 306 | 307 | public int NumberofMembers { get; set; } 308 | 309 | public int NumberofExternalMembers { get; set; } 310 | 311 | public int NumberofInternalMembers { get; set; } 312 | 313 | public int NumberofPureAureInternalMembers { get; set; } 314 | 315 | public int NumberofSyncInternalMembers { get; set; } 316 | 317 | public List ExternalTenantInformation { get; set; } 318 | 319 | 320 | public bool? AzureADConnectDirSyncConfigured { get; set; } 321 | public bool? AzureADConnectDirSyncEnabled { get; set; } 322 | public int AzureADConnectFederatedDomainCount { get; set; } 323 | public int? AzureADConnectNumberOfHoursFromLastSync { get; set; } 324 | public bool? AzureADConnectPassThroughAuthenticationEnabled { get; set; } 325 | public bool? AzureADConnectSeamlessSingleSignOnEnabled { get; set; } 326 | public int AzureADConnectVerifiedCustomDomainCount { get; set; } 327 | public int AzureADConnectVerifiedDomainCount { get; set; } 328 | 329 | public string OnPremiseDomainSid { get; set; } 330 | public List Applications { get; set; } 331 | public bool UsersPermissionToCreateGroupsEnabled { get; set; } 332 | public bool UsersPermissionToCreateLOBAppsEnabled { get; set; } 333 | public bool UsersPermissionToReadOtherUsersEnabled { get; set; } 334 | public bool UsersPermissionToUserConsentToAppEnabled { get; set; } 335 | 336 | public string PolicyGuestUserRoleId { get; set; } 337 | public bool? PolicyAllowEmailVerifiedUsersToJoinOrganization { get; set; } 338 | 339 | public List ForwardingMailboxes { get; set; } 340 | public int GlobalScore { get; set; } 341 | public int MaturityLevel { get; set; } 342 | public List RiskRules { get; set; } 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /Credentials/CertificateCredential.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using Microsoft.Win32.SafeHandles; 8 | using PingCastleCloud.RESTServices.Azure; 9 | using PingCastleCloud.Tokens; 10 | using System; 11 | using System.Collections.Generic; 12 | using System.ComponentModel; 13 | using System.Diagnostics; 14 | using System.IO; 15 | using System.Linq; 16 | using System.Runtime.ConstrainedExecution; 17 | using System.Runtime.InteropServices; 18 | using System.Security.Cryptography; 19 | using System.Security.Cryptography.X509Certificates; 20 | using System.Text; 21 | using System.Threading.Tasks; 22 | 23 | namespace PingCastleCloud.Credentials 24 | { 25 | public class CertificateCredential : IDisposable, IAzureCredential 26 | { 27 | private CertificateCredential() 28 | { 29 | 30 | } 31 | 32 | public static CertificateCredential LoadFromCertificate(string clientId, string tenantId, X509Certificate2 certificate) 33 | { 34 | if (!certificate.HasPrivateKey) 35 | throw new ApplicationException("Certificate without private key"); 36 | 37 | var cred = new CertificateCredential(); 38 | cred.PrivateKey = certificate.GetRSAPrivateKey(); 39 | cred.ThumbPrint = certificate.Thumbprint; 40 | cred.ClientId = clientId; 41 | cred.Tenantid = tenantId; 42 | return cred; 43 | } 44 | 45 | public static CertificateCredential LoadFromKeyFile(string clientId, string tenantId, string key, string thumbPrint) 46 | { 47 | byte[] keyB = StringToBinary(key); 48 | var cred = new CertificateCredential(); 49 | cred.PrivateKey = DecodePKCS8Blob(keyB); 50 | cred.ThumbPrint = thumbPrint; 51 | cred.ClientId = clientId; 52 | cred.Tenantid = tenantId; 53 | return cred; 54 | } 55 | 56 | public static CertificateCredential LoadFromP12(string clientId, string tenantId, string p12file, string password) 57 | { 58 | var data = File.ReadAllBytes(p12file); 59 | IntPtr buffer = Marshal.AllocHGlobal(data.Length); 60 | IntPtr handle; 61 | try 62 | { 63 | Marshal.Copy(data, 0, buffer, data.Length); 64 | var pfxBlob = new CRYPTOAPI_BLOB { cbData = data.Length, pbData = buffer }; 65 | handle = PFXImportCertStore(ref pfxBlob, password, CRYPT_EXPORTABLE | CRYPT_USER_KEYSET); 66 | if (handle == IntPtr.Zero) 67 | throw new Win32Exception(); 68 | } 69 | finally 70 | { 71 | Marshal.FreeHGlobal(buffer); 72 | } 73 | using (var store = new X509Store(handle)) 74 | { 75 | foreach (var certificate in store.Certificates) 76 | { 77 | if (certificate.HasPrivateKey) 78 | { 79 | var cred = new CertificateCredential(); 80 | cred.PrivateKey = certificate.GetRSAPrivateKey(); 81 | cred.ThumbPrint = certificate.Thumbprint; 82 | cred.ClientId = clientId; 83 | cred.Tenantid = tenantId; 84 | return cred; 85 | } 86 | } 87 | store.Close(); 88 | } 89 | throw new ApplicationException("No private key found in pfx file"); 90 | } 91 | 92 | public async Task GetToken() where T : IAzureService 93 | { 94 | return await TokenFactory.GetToken(this); 95 | } 96 | 97 | public string ClientId { get; private set; } 98 | public string Tenantid { get; set; } 99 | public string TenantidToQuery { get; set; } 100 | public RSA PrivateKey { get; private set; } 101 | public string ThumbPrint { get; private set; } 102 | 103 | public Token LastTokenQueried { get; private set; } 104 | 105 | public void Dispose() 106 | { 107 | if (PrivateKey != null) 108 | PrivateKey.Dispose(); 109 | PrivateKey = null; 110 | } 111 | 112 | const int CRYPT_EXPORTABLE = 1; 113 | const int CRYPT_USER_KEYSET = 0x1000; 114 | 115 | 116 | [DllImport("crypt32.dll", SetLastError = true)] 117 | static extern IntPtr PFXImportCertStore(ref CRYPTOAPI_BLOB pPfx, [MarshalAs(UnmanagedType.LPWStr)] String szPassword, uint dwFlags); 118 | 119 | enum CRYPT_STRING_FLAGS : uint 120 | { 121 | CRYPT_STRING_BASE64_ANY = 6, 122 | } 123 | 124 | [DllImport("crypt32.dll", SetLastError = true, CharSet = CharSet.Unicode)] 125 | [return: MarshalAs(UnmanagedType.Bool)] 126 | static extern bool CryptStringToBinary([MarshalAs(UnmanagedType.LPWStr)] string pszString, int cchString, CRYPT_STRING_FLAGS dwFlags, [Out] IntPtr pbBinary, ref int pcbBinary, out int pdwSkip, out int pdwFlags); 127 | static byte[] StringToBinary(string datastr) 128 | { 129 | int flags, skipbytes, buflen; 130 | 131 | buflen = datastr.Length; 132 | IntPtr buffer = Marshal.AllocHGlobal(buflen); 133 | try 134 | { 135 | bool status = CryptStringToBinary(datastr, 136 | datastr.Length, 137 | CRYPT_STRING_FLAGS.CRYPT_STRING_BASE64_ANY, 138 | buffer, 139 | ref buflen, 140 | out skipbytes, 141 | out flags); 142 | 143 | if (!status) 144 | { 145 | Trace.WriteLine("Unable to decode the private key"); 146 | throw new Win32Exception(); 147 | } 148 | byte[] keybytes = new byte[buflen]; 149 | Marshal.Copy(buffer, keybytes, 0, (int)buflen); 150 | return keybytes; 151 | } 152 | finally 153 | { 154 | Marshal.FreeHGlobal(buffer); 155 | } 156 | } 157 | 158 | const int X509_ASN_ENCODING = 0x00000001; 159 | const int PKCS_7_ASN_ENCODING = 0x00010000; 160 | 161 | sealed class LocalAllocHandle : SafeHandleZeroOrMinusOneIsInvalid 162 | { 163 | private LocalAllocHandle() : base(ownsHandle: true) { } 164 | 165 | public static LocalAllocHandle Alloc(int cb) 166 | { 167 | LocalAllocHandle handle = new LocalAllocHandle(); 168 | handle.AllocCore(cb); 169 | return handle; 170 | } 171 | 172 | [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] 173 | private void AllocCore(int cb) 174 | { 175 | SetHandle(Marshal.AllocHGlobal(cb)); 176 | } 177 | 178 | protected override bool ReleaseHandle() 179 | { 180 | Marshal.FreeHGlobal(handle); 181 | return true; 182 | } 183 | } 184 | 185 | 186 | [DllImport("crypt32.dll", SetLastError = true)] 187 | [return: MarshalAs(UnmanagedType.Bool)] 188 | static extern bool CryptDecodeObjectEx( 189 | uint dwCertEncodingType, 190 | uint lpszStructType, 191 | IntPtr pbEncoded, 192 | int cbEncoded, 193 | uint dwFlags, 194 | IntPtr pDecodePara, 195 | out LocalAllocHandle pvStructInfo, 196 | out uint pcbStructInfo); 197 | 198 | const uint CNG_RSA_PRIVATE_KEY_BLOB = 83; 199 | const uint PKCS_PRIVATE_KEY_INFO = 44; 200 | const uint CRYPT_DECODE_ALLOC_FLAG = 0x8000; 201 | 202 | [StructLayout(LayoutKind.Sequential)] 203 | struct CRYPT_ALGORITHM_IDENTIFIER 204 | { 205 | [MarshalAs(UnmanagedType.LPStr)] 206 | public string pszObjId; 207 | CRYPTOAPI_BLOB Parameters; 208 | } 209 | 210 | [StructLayout(LayoutKind.Sequential)] 211 | struct CRYPTOAPI_BLOB 212 | { 213 | public int cbData; 214 | public IntPtr pbData; 215 | } 216 | 217 | [StructLayout(LayoutKind.Sequential)] 218 | struct CRYPT_PRIVATE_KEY_INFO 219 | { 220 | public int Version; 221 | public CRYPT_ALGORITHM_IDENTIFIER Algorithm; 222 | public CRYPTOAPI_BLOB PrivateKey; 223 | } 224 | 225 | [StructLayout(LayoutKind.Sequential)] 226 | struct BCRYPT_RSAKEY_BLOB 227 | { 228 | public int Magic; 229 | public int BitLength; 230 | public int cbPublicExp; 231 | public int cbModulus; 232 | public int cbPrime1; 233 | public int cbPrime2; 234 | } 235 | 236 | static RSA DecodePKCS8Blob(byte[] derBlob) 237 | { 238 | using (var derBlobHandle = LocalAllocHandle.Alloc(derBlob.Length)) 239 | { 240 | Marshal.Copy( 241 | derBlob, 242 | 0, 243 | derBlobHandle.DangerousGetHandle(), 244 | derBlob.Length); 245 | 246 | // 247 | // Decode RSA PublicKey DER -> CSP blob. 248 | // 249 | if (CryptDecodeObjectEx( 250 | X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, 251 | PKCS_PRIVATE_KEY_INFO, 252 | derBlobHandle.DangerousGetHandle(), 253 | derBlob.Length, 254 | CRYPT_DECODE_ALLOC_FLAG, 255 | IntPtr.Zero, 256 | out var keyBlobHandle, 257 | out var keyBlobSize)) 258 | { 259 | var keyInfo = (CRYPT_PRIVATE_KEY_INFO)Marshal.PtrToStructure(keyBlobHandle.DangerousGetHandle(), typeof(CRYPT_PRIVATE_KEY_INFO)); 260 | byte[] data = new byte[keyInfo.PrivateKey.cbData]; 261 | Marshal.Copy(keyInfo.PrivateKey.pbData, data, 0, keyInfo.PrivateKey.cbData); 262 | 263 | var h = LocalAllocHandle.Alloc(keyInfo.PrivateKey.cbData); 264 | Marshal.Copy(data, 0, h.DangerousGetHandle(), keyInfo.PrivateKey.cbData); 265 | 266 | return PKCS8BlobToCNG(h, keyInfo.PrivateKey.cbData); 267 | } 268 | else 269 | { 270 | throw new CryptographicException( 271 | "Failed to decode DER blob", 272 | new Win32Exception()); 273 | } 274 | } 275 | } 276 | 277 | static RSA PKCS8BlobToCNG(LocalAllocHandle BlobHandle, int BlobSize) 278 | { 279 | 280 | // 281 | // Decode RSA PublicKey DER -> CSP blob. 282 | // 283 | if (CryptDecodeObjectEx( 284 | X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, 285 | CNG_RSA_PRIVATE_KEY_BLOB, 286 | BlobHandle.DangerousGetHandle(), 287 | BlobSize, 288 | CRYPT_DECODE_ALLOC_FLAG, 289 | IntPtr.Zero, 290 | out var keyBlobHandle, 291 | out var keyBlobSize)) 292 | { 293 | return BlobToRSA(keyBlobHandle); 294 | } 295 | else 296 | { 297 | throw new CryptographicException( 298 | "Failed to decode DER blob", 299 | new Win32Exception()); 300 | } 301 | } 302 | 303 | static byte[] ReadData(IntPtr data, int offset, int size) 304 | { 305 | byte[] managedArray = new byte[size]; 306 | Marshal.Copy(new IntPtr(data.ToInt64() + offset), managedArray, 0, size); 307 | return managedArray; 308 | } 309 | static RSA BlobToRSA(LocalAllocHandle keyBlobHandle) 310 | { 311 | IntPtr data = keyBlobHandle.DangerousGetHandle(); 312 | var blob = (BCRYPT_RSAKEY_BLOB)Marshal.PtrToStructure(data, typeof(BCRYPT_RSAKEY_BLOB)); 313 | 314 | // https://docs.microsoft.com/en-us/windows/win32/api/bcrypt/ns-bcrypt-bcrypt_rsakey_blob 315 | var param = new RSAParameters(); 316 | if (blob.Magic != 0x32415352 && blob.Magic != 0x33415352) 317 | { 318 | throw new ApplicationException("Invalid blob"); 319 | } 320 | int offset = Marshal.SizeOf(blob); 321 | param.Exponent = ReadData(data, offset, blob.cbPublicExp); 322 | offset += blob.cbPublicExp; 323 | param.Modulus = ReadData(data, offset, blob.cbModulus); 324 | offset += blob.cbModulus; 325 | param.P = ReadData(data, offset, blob.cbPrime1); 326 | offset += blob.cbPrime1; 327 | param.Q = ReadData(data, offset, blob.cbPrime2); 328 | offset += blob.cbPrime2; 329 | /*if (blob.Magic == 0x33415352) 330 | { 331 | }*/ 332 | 333 | var output = new RSACng(); 334 | output.ImportParameters(param); 335 | return output; 336 | } 337 | } 338 | } 339 | 340 | -------------------------------------------------------------------------------- /Logs/SazGenerator.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) Vincent LE TOUX for Ping Castle. All rights reserved. 3 | // https://www.pingcastle.com 4 | // 5 | // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. 6 | // 7 | using System; 8 | using System.Collections.Generic; 9 | using System.IO; 10 | using System.IO.Compression; 11 | using System.IO.Packaging; 12 | using System.Linq; 13 | using System.Net.Http; 14 | using System.Net.Http.Headers; 15 | using System.Text; 16 | using System.Threading.Tasks; 17 | using System.Xml.Serialization; 18 | 19 | namespace PingCastleCloud.Logs 20 | { 21 | public class SazGenerator : IDisposable 22 | { 23 | public SazGenerator() : this("log.saz") 24 | { 25 | 26 | } 27 | public SazGenerator(string filename) 28 | { 29 | CreateFile(filename); 30 | } 31 | 32 | Package archive; 33 | Dictionary entries = new Dictionary(); 34 | 35 | int entryNum = 1; 36 | 37 | public string SessionComment { get; set; } 38 | 39 | class sazentry 40 | { 41 | public Uri uri { get; set; } 42 | public DateTime StartTime { get; internal set; } 43 | } 44 | 45 | private void CreateFile(string filename) 46 | { 47 | archive = Package.Open(filename, 48 | System.IO.FileMode.Create, 49 | System.IO.FileAccess.ReadWrite); 50 | 51 | PackagePart indexPackagePart = archive.CreatePart(new Uri("/_index.htm", UriKind.Relative), "text/html"); 52 | } 53 | 54 | private static Uri UriForPart(int sessionId, char mode) 55 | { 56 | return new Uri(string.Format("/raw/{0}_{1}.{2}", sessionId, mode, (mode == 'm') ? "xml" : "txt"), UriKind.Relative); 57 | } 58 | 59 | // called from multiple threads 60 | public int RecordBeginQuery(HttpRequestMessage request) 61 | { 62 | lock (entries) 63 | { 64 | entries.Add(entryNum, new sazentry { uri = request.RequestUri, StartTime = DateTime.Now }); 65 | 66 | var requestPart = archive.CreatePart(UriForPart(entryNum, 'c'), "text/plain"); 67 | using (var requestStream = requestPart.GetStream(System.IO.FileMode.Create)) 68 | { 69 | var b = RequestMessageToString(request); 70 | requestStream.Write(b, 0, b.Length); 71 | if (request.Content != null) 72 | { 73 | request.Content.CopyToAsync(requestStream).GetAwaiter().GetResult(); 74 | } 75 | } 76 | return entryNum++; 77 | } 78 | } 79 | 80 | // called from multiple threads 81 | public void RecordEndQuery(int sessionId, HttpResponseMessage response) 82 | { 83 | //copy the response stream to a stream which can be rewinded 84 | var ms = new MemoryStream(); 85 | response.Content.CopyToAsync(ms).GetAwaiter().GetResult(); 86 | ms.Position = 0; 87 | 88 | var rc = response.Content; 89 | response.Content = new StreamContent(ms); 90 | 91 | lock (entries) 92 | { 93 | var entry = entries[sessionId]; 94 | TimeSpan duration = DateTimeOffset.UtcNow.Subtract(entry.StartTime); 95 | 96 | var responsePart = archive.CreatePart(UriForPart(sessionId, 's'), "text/plain"); 97 | 98 | using (var responseStream = responsePart.GetStream(System.IO.FileMode.Create)) 99 | { 100 | var b = ResponseMessageToString(response); 101 | responseStream.Write(b, 0, b.Length); 102 | if (response.Content != null) 103 | { 104 | response.Content.CopyToAsync(responseStream).GetAwaiter().GetResult(); 105 | } 106 | } 107 | 108 | var metadataPart = archive.CreatePart(UriForPart(sessionId, 'm'), "application/xml"); 109 | var metadata = CreateMetadataForSession(sessionId, entry.StartTime, duration); 110 | using (var metadataStream = metadataPart.GetStream(System.IO.FileMode.Create)) 111 | { 112 | metadata.WriteToStream(metadataStream); 113 | } 114 | } 115 | } 116 | 117 | byte[] RequestMessageToString(HttpRequestMessage request) 118 | { 119 | var sb = new StringBuilder(); 120 | SerializeRequestLine(sb, request); 121 | SerializeHeaderFields(sb, request.Headers); 122 | if (request.Content != null) 123 | { 124 | SerializeHeaderFields(sb, request.Content.Headers); 125 | } 126 | sb.Append("\r\n"); 127 | return Encoding.UTF8.GetBytes(sb.ToString()); 128 | } 129 | 130 | 131 | byte[] ResponseMessageToString(HttpResponseMessage response) 132 | { 133 | var sb = new StringBuilder(); 134 | SerializeStatusLine(sb, response); 135 | SerializeHeaderFields(sb, response.Headers); 136 | if (response.Content != null) 137 | { 138 | SerializeHeaderFields(sb, response.Content.Headers); 139 | } 140 | sb.Append("\r\n"); 141 | return Encoding.UTF8.GetBytes(sb.ToString()); 142 | } 143 | 144 | private static void SerializeRequestLine(StringBuilder message, HttpRequestMessage httpRequest) 145 | { 146 | message.Append(httpRequest.Method + " "); 147 | message.Append(httpRequest.RequestUri.PathAndQuery + " "); 148 | message.Append("HTTP/" + ((httpRequest.Version != null) ? httpRequest.Version.ToString(2) : "1.1") + "\r\n"); 149 | if (httpRequest.Headers.Host == null) 150 | { 151 | message.Append("Host: " + httpRequest.RequestUri.Authority + "\r\n"); 152 | } 153 | } 154 | 155 | private static void SerializeHeaderFields(StringBuilder message, HttpHeaders headers) 156 | { 157 | if (headers != null) 158 | { 159 | foreach (KeyValuePair> header in headers) 160 | { 161 | message.Append(header.Key + ": " + string.Join(", ", header.Value) + "\r\n"); 162 | } 163 | } 164 | } 165 | private static void SerializeStatusLine(StringBuilder message, HttpResponseMessage httpResponse) 166 | { 167 | message.Append("HTTP/" + ((httpResponse.Version != null) ? httpResponse.Version.ToString(2) : "1.1") + " "); 168 | message.Append((int)httpResponse.StatusCode + " "); 169 | message.Append(httpResponse.ReasonPhrase + "\r\n"); 170 | } 171 | 172 | 173 | private SessionMetadata CreateMetadataForSession(int sessionId, DateTimeOffset startTime, TimeSpan duration) 174 | { 175 | var metadata = new SessionMetadata 176 | { 177 | SessionID = sessionId, 178 | BitFlags = 59 179 | }; 180 | 181 | metadata.PipeInfo = new PipeInfo { Streamed = true, Reused = false, CltReuse = false }; 182 | const string format = @"yyyy-MM-ddTHH\:mm\:ss.fffffffzzz"; 183 | metadata.SessionTimers = new SessionTimers 184 | { 185 | ClientConnected = startTime.ToString(format), 186 | ClientBeginRequest = startTime.ToString(format), 187 | GotRequestHeaders = startTime.ToString(format), 188 | ClientDoneRequest = startTime.ToString(format), 189 | ServerConnected = startTime.ToString(format), 190 | FiddlerBeginRequest = startTime.ToString(format), 191 | ServerGotRequest = startTime.ToString(format), 192 | ServerBeginResponse = startTime.Add(duration).ToString(format), 193 | GotResponseHeaders = startTime.Add(duration).ToString(format), 194 | ServerDoneResponse = startTime.Add(duration).ToString(format), 195 | ClientBeginResponse = startTime.Add(duration).ToString(format), 196 | ClientDoneResponse = startTime.Add(duration).ToString(format) 197 | }; 198 | 199 | metadata.SessionFlags.Add(new SessionFlag { Name = SessionFlag.ClientIP, Value = "127.0.0.1" }); 200 | metadata.SessionFlags.Add(new SessionFlag { Name = SessionFlag.ProcessInfo, Value = "pingcastlecloud.exe:1234" }); 201 | if (!string.IsNullOrEmpty(SessionComment)) 202 | { 203 | metadata.SessionFlags.Add(new SessionFlag { Name = SessionFlag.Comment, Value = SessionComment }); 204 | } 205 | return metadata; 206 | } 207 | 208 | 209 | void CloseFile() 210 | { 211 | archive.Close(); 212 | archive = null; 213 | } 214 | 215 | // Other functions go here... 216 | 217 | public void Dispose() 218 | { 219 | Dispose(true); 220 | GC.SuppressFinalize(this); 221 | } 222 | 223 | protected virtual void Dispose(bool disposing) 224 | { 225 | if (disposing) 226 | { 227 | // free managed resources 228 | CloseFile(); 229 | } 230 | // free native resources if there are any. 231 | } 232 | 233 | [XmlRoot("Session")] 234 | public class SessionMetadata 235 | { 236 | public SessionMetadata() 237 | { 238 | this.SessionFlags = new List(); 239 | } 240 | 241 | [XmlAttribute("SID")] 242 | public int SessionID { get; set; } 243 | 244 | [XmlAttribute] 245 | public int BitFlags { get; set; } 246 | 247 | [XmlElement] 248 | public SessionTimers SessionTimers { get; set; } 249 | 250 | [XmlElement] 251 | public PipeInfo PipeInfo { get; set; } 252 | 253 | [XmlArray("SessionFlags")] 254 | public List SessionFlags { get; set; } 255 | 256 | internal void WriteToStream(Stream metadataStream) 257 | { 258 | XmlSerializer serialize = new XmlSerializer(this.GetType()); 259 | serialize.Serialize(metadataStream, this); 260 | } 261 | } 262 | 263 | public class PipeInfo 264 | { 265 | [XmlAttribute("Streamed")] 266 | public bool Streamed 267 | { 268 | get; set; 269 | } 270 | 271 | [XmlAttribute("CltReuse")] 272 | public bool CltReuse 273 | { 274 | get; set; 275 | } 276 | 277 | [XmlAttribute("Reused")] 278 | public bool Reused { get; set; } 279 | } 280 | 281 | public class SessionFlag 282 | { 283 | [XmlAttribute("N")] 284 | public string Name { get; set; } 285 | 286 | [XmlAttribute("V")] 287 | public string Value { get; set; } 288 | 289 | public const string EgressPort = "x-egressport"; 290 | public const string ResponseBodyTransferLength = "x-responsebodytransferlength"; 291 | public const string ClientPort = "x-clientport"; 292 | public const string ClientIP = "x-clientip"; 293 | public const string ServerSocket = "x-serversocket"; 294 | public const string HostIP = "x-hostip"; 295 | public const string ProcessInfo = "x-processinfo"; 296 | public const string Comment = "ui-comments"; 297 | 298 | } 299 | 300 | public class SessionTimers 301 | { 302 | [XmlAttribute] 303 | public string ClientConnected { get; set; } 304 | 305 | [XmlAttribute] 306 | public string ClientBeginRequest { get; set; } 307 | 308 | [XmlAttribute] 309 | public string GotRequestHeaders { get; set; } 310 | 311 | [XmlAttribute] 312 | public string ClientDoneRequest { get; set; } 313 | 314 | [XmlAttribute] 315 | public int GatewayTime { get; set; } 316 | 317 | [XmlAttribute] 318 | public int DNSTime { get; set; } 319 | 320 | [XmlAttribute] 321 | public int TCPConnectTime { get; set; } 322 | 323 | [XmlAttribute] 324 | public int HTTPSHandshakeTime { get; set; } 325 | 326 | [XmlAttribute] 327 | public string ServerConnected { get; set; } 328 | 329 | [XmlAttribute] 330 | public string FiddlerBeginRequest { get; set; } 331 | 332 | [XmlAttribute] 333 | public string ServerGotRequest { get; set; } 334 | 335 | [XmlAttribute] 336 | public string ServerBeginResponse { get; set; } 337 | 338 | [XmlAttribute] 339 | public string GotResponseHeaders { get; set; } 340 | 341 | [XmlAttribute] 342 | public string ServerDoneResponse { get; set; } 343 | 344 | [XmlAttribute] 345 | public string ClientBeginResponse { get; set; } 346 | 347 | [XmlAttribute] 348 | public string ClientDoneResponse { get; set; } 349 | 350 | } 351 | } 352 | } 353 | --------------------------------------------------------------------------------