From 7b63f846076baa96147df04b135dd31eef8f5fac Mon Sep 17 00:00:00 2001 From: vletoux Date: Wed, 30 Jan 2019 21:58:09 +0100 Subject: [PATCH] PingCastle 2.6.0.0 --- ADWS/ADConnection.cs | 20 +- ADWS/ADDomainInfo.cs | 7 +- ADWS/ADItem.cs | 14 + ADWS/ADWSConnection.cs | 76 +- ADWS/ADWebService.cs | 33 +- {Healthcheck => ADWS}/DomainLocator.cs | 2 +- ADWS/IADConnection.cs | 14 + ADWS/LDAPConnection.cs | 34 +- ConsoleMenu.cs | 321 +++ Data/CompromiseGraphData.cs | 243 +- Data/DataHelper.cs | 35 +- {Healthcheck => Data}/DomainKey.cs | 32 +- Data/HealthCheckBUEntityData.cs | 1 + Data/HealthcheckData.cs | 324 +-- Data/HealthcheckDataCollection.cs | 134 - Data/IPingCastleReport.cs | 24 + Data/IPingCastleReportGenerator.cs | 22 + Data/PingCastleReportCollection.cs | 199 ++ Data/PingCastleReportDataExportLevel.cs | 14 + ...ataHelper.cs => PingCastleReportHelper.cs} | 35 +- ...s => PingCastleReportHistoryCollection.cs} | 55 +- Graph/Database/IDataStorage.cs | 32 +- Graph/Database/LiveDataStorage.cs | 137 +- Graph/Database/MappingType.cs | 2 +- Graph/Database/Node.cs | 5 +- Graph/Database/Relation.cs | 2 +- Graph/Database/RelationType.cs | 2 +- .../ExportDataFromActiveDirectoryLive.cs | 234 +- Graph/Export/RelationFactory.cs | 29 +- Graph/Reporting/GraphObjectReference.cs | 68 + Graph/Reporting/ReportGenerator.cs | 708 ++++- ...ompromiseGraphAnomalyIndirectCritical10.cs | 25 + ...mpromiseGraphAnomalyIndirectCritical100.cs | 25 + ...ompromiseGraphAnomalyIndirectCritical50.cs | 25 + ...mpromiseGraphAnomalyIndirectCriticalAny.cs | 26 + .../CompromiseGraphAnomalyIndirectHigh10.cs | 25 + .../CompromiseGraphAnomalyIndirectHigh100.cs | 25 + .../CompromiseGraphAnomalyIndirectHigh50.cs | 25 + .../CompromiseGraphAnomalyIndirectHighAny.cs | 26 + .../CompromiseGraphAnomalyIndirectMedium10.cs | 25 + ...CompromiseGraphAnomalyIndirectMedium100.cs | 25 + .../CompromiseGraphAnomalyIndirectMedium50.cs | 25 + ...CompromiseGraphAnomalyIndirectMediumAny.cs | 26 + .../CompromiseGraphAnomalyIndirectUser.cs | 41 + ...CompromiseGraphPrivilegedOperatorsEmpty.cs | 40 + Graph/Rules/CompromiseGraphRule.cs | 61 + .../CompromiseGraphStalePermissionsCleanup.cs | 32 + ...omiseGraphTrustChildDomainHasPermission.cs | 29 + ...phTrustMoreThanOneDomainControlCritical.cs | 25 + ...omiseGraphTrustOneDomainControlCritical.cs | 46 + ...seGraphTrustOneDomainControlUserDefined.cs | 35 + Graph/Rules/RuleDescription.resx | 243 ++ Healthcheck/ADModel.cs | 59 +- Healthcheck/HealthCheckEncryption.cs | 509 ++-- Healthcheck/HealthcheckAnalyzer.cs | 808 +++--- Healthcheck/HealthcheckRules.cs | 203 -- .../HeatlcheckRuleAnomalyAdminSDHolder.cs | 22 +- ...lcheckRuleAnomalyAnonymousAuthorizedGPO.cs | 7 +- .../HeatlcheckRuleAnomalyBackupMetadata.cs | 9 +- ...eatlcheckRuleAnomalyCertMD2Intermediate.cs | 7 +- .../Rules/HeatlcheckRuleAnomalyCertMD2Root.cs | 7 +- ...eatlcheckRuleAnomalyCertMD4Intermediate.cs | 7 +- .../Rules/HeatlcheckRuleAnomalyCertMD4Root.cs | 7 +- ...eatlcheckRuleAnomalyCertMD5Intermediate.cs | 7 +- .../Rules/HeatlcheckRuleAnomalyCertMD5Root.cs | 7 +- ...atlcheckRuleAnomalyCertSHA0Intermediate.cs | 7 +- .../HeatlcheckRuleAnomalyCertSHA0Root.cs | 7 +- ...atlcheckRuleAnomalyCertSHA1Intermediate.cs | 7 +- .../HeatlcheckRuleAnomalyCertSHA1Root.cs | 7 +- .../Rules/HeatlcheckRuleAnomalyCertWeakRSA.cs | 7 +- .../Rules/HeatlcheckRuleAnomalyDCSpooler.cs | 30 + ...tlcheckRuleAnomalyDsHeuristicsAnonymous.cs | 9 +- .../Rules/HeatlcheckRuleAnomalyKrbtgt.cs | 14 +- .../Rules/HeatlcheckRuleAnomalyLAPS.cs | 10 +- .../Rules/HeatlcheckRuleAnomalyLMHash.cs | 9 +- ...HeatlcheckRuleAnomalyMembershipEveryone.cs | 7 +- .../HeatlcheckRuleAnomalyMinPasswordLen.cs | 8 +- .../Rules/HeatlcheckRuleAnomalyNotEnoughDC.cs | 23 + .../Rules/HeatlcheckRuleAnomalyNullSession.cs | 8 +- .../HeatlcheckRuleAnomalyPasswordInGPO.cs | 8 +- ...eatlcheckRuleAnomalyPreWin2000Anonymous.cs | 10 +- ...lcheckRuleAnomalyRemoteBlankPasswordUse.cs | 7 +- ...HeatlcheckRuleAnomalyReversiblePassword.cs | 7 +- ...checkRuleAnomalySMB2SignatureNotEnabled.cs | 9 +- ...heckRuleAnomalySMB2SignatureNotRequired.cs | 9 +- ...atlcheckRuleAnomalySchemaProtectedUsers.cs | 10 +- .../HeatlcheckRuleAnomalyServicePolicy.cs | 13 +- .../HeatlcheckRuleAnomalySmartCardRequired.cs | 10 +- Healthcheck/Rules/HeatlcheckRuleAttribute.cs | 134 - .../HeatlcheckRulePrivilegedAdminInactive.cs | 10 +- .../HeatlcheckRulePrivilegedAdminLogin.cs | 7 +- .../HeatlcheckRulePrivilegedAdminNumber.cs | 9 +- .../Rules/HeatlcheckRulePrivilegedDCOwner.cs | 7 +- ...lcheckRulePrivilegedDangerousDelegation.cs | 8 +- .../HeatlcheckRulePrivilegedDelegated.cs | 9 +- ...tlcheckRulePrivilegedDelegationEveryone.cs | 10 +- ...atlcheckRulePrivilegedDelegationGPOData.cs | 28 + ...tlcheckRulePrivilegedDelegationKeyAdmin.cs | 31 + ...heckRulePrivilegedDelegationLoginScript.cs | 8 +- ...heckRulePrivilegedExchangeAdminSDHolder.cs | 32 + .../HeatlcheckRulePrivilegedOperatorsEmpty.cs | 34 + ...atlcheckRulePrivilegedPrivilegeEveryone.cs | 45 + .../HeatlcheckRulePrivilegedSchemaAdmins.cs | 10 +- ...tlcheckRulePrivilegedServiceDomainAdmin.cs | 10 +- ...ckRulePrivilegedUnconstrainedDelegation.cs | 48 + ...atlcheckRulePrivilegedUnknownDelegation.cs | 7 +- ...eatlcheckRuleStaleADRegistrationEnabled.cs | 9 +- .../Rules/HeatlcheckRuleStaledDCNotUpdated.cs | 8 +- .../HeatlcheckRuleStaledDCSubnetMissing.cs | 9 +- .../Rules/HeatlcheckRuleStaledDesEnabled.cs | 9 +- .../HeatlcheckRuleStaledDuplicateObjects.cs | 7 +- .../Rules/HeatlcheckRuleStaledInactive.cs | 8 +- .../HeatlcheckRuleStaledInactiveComputer.cs | 12 +- .../Rules/HeatlcheckRuleStaledMS14_068.cs | 8 +- .../Rules/HeatlcheckRuleStaledMS17_010.cs | 8 +- .../Rules/HeatlcheckRuleStaledNoPreAuth.cs | 7 +- .../Rules/HeatlcheckRuleStaledObsolete2000.cs | 8 +- .../Rules/HeatlcheckRuleStaledObsolete2003.cs | 12 +- .../HeatlcheckRuleStaledObsoleteDC2000.cs | 10 +- .../HeatlcheckRuleStaledObsoleteDC2003.cs | 10 +- .../Rules/HeatlcheckRuleStaledObsoleteNT4.cs | 8 +- .../Rules/HeatlcheckRuleStaledObsoleteXP.cs | 12 +- .../Rules/HeatlcheckRuleStaledPrimaryGroup.cs | 7 +- ...eatlcheckRuleStaledPrimaryGroupComputer.cs | 7 +- .../HeatlcheckRuleStaledPwdNotRequired.cs | 8 +- ...eatlcheckRuleStaledReversibleEncryption.cs | 7 +- ...kRuleStaledReversibleEncryptionComputer.cs | 7 +- .../Rules/HeatlcheckRuleStaledSIDHistory.cs | 9 +- .../Rules/HeatlcheckRuleStaledSMBv1.cs | 10 +- .../Rules/HeatlcheckRuleTrustDownlevel.cs | 7 +- .../Rules/HeatlcheckRuleTrustInactive.cs | 7 +- ...atlcheckRuleTrustLoginScriptOutOfDomain.cs | 7 +- .../Rules/HeatlcheckRuleTrustSIDFiltering.cs | 16 +- ...tlcheckRuleTrustSIDHistoryAuditingGroup.cs | 8 +- ...HeatlcheckRuleTrustSIDHistorySameDomain.cs | 8 +- ...tlcheckRuleTrustSIDHistoryUnknownDomain.cs | 8 +- Healthcheck/Rules/RuleDescription.resx | 444 +++- License.cs | 6 +- NativeMethods.cs | 62 +- PingCastle.csproj | 107 +- PingCastleException.cs | 31 + PingCastleFactory.cs | 95 + Program.cs | 1922 +++++++------- Properties/AssemblyInfo.cs | 6 +- {NullSession => RPC}/lsa.cs | 3 +- {NullSession => RPC}/nativemethods.cs | 19 +- {NullSession => RPC}/nrpc.cs | 5 +- {NullSession => RPC}/nullsession.cs | 2 +- {NullSession => RPC}/rpcapi.cs | 53 +- {NullSession => RPC}/samr.cs | 3 +- RPC/spool.cs | 372 +++ Report/HealthCheckReportBase.cs | 552 ---- Report/HealthCheckReportCompromiseGraph.cs | 569 ---- Report/HealthCheckReportConsolidation.cs | 1396 ---------- Report/HealthCheckReportSingle.cs | 2351 ----------------- Report/IPingCastleReportUser.cs | 16 + Report/ReportBase.cs | 715 +++++ Report/ReportCompromiseGraph.cs | 1416 ++++++++++ Report/ReportCompromiseGraphConsolidation.cs | 288 ++ Report/ReportHealthCheckConsolidation.cs | 1360 ++++++++++ Report/ReportHealthCheckSingle.cs | 1961 ++++++++++++++ Report/ReportHelper.cs | 178 ++ ...eportMapBuilder.cs => ReportMapBuilder.cs} | 196 +- Report/ReportRiskControls.cs | 453 ++++ Rules/IRiskEvaluation.cs | 26 + Rules/IRuleScore.cs | 18 + Rules/RiskModelCategory.cs | 75 + Rules/RiskModelObjective.cs | 27 + Rules/RiskRuleCategory.cs | 20 + Rules/RuleAttribute.cs | 249 ++ .../RuleBase.cs | 123 +- Rules/RuleSet.cs | 192 ++ Scanners/ACLScanner.cs | 427 +++ Scanners/AccountEnumeratorViaTrust.cs | 118 - Scanners/ConsistencyScanner.cs | 95 + Scanners/ForeignUsersScanner.cs | 215 ++ Scanners/IScanner.cs | 15 +- Scanners/NullSessionScanner.cs | 92 + Scanners/ReplicationScanner.cs | 5 + Scanners/ScannerBase.cs | 56 +- Scanners/Smb1Protocol.cs | 193 ++ Scanners/Smb2Protocol.cs | 413 +++ Scanners/SmbScanner.cs | 345 +-- Scanners/SpoolerScanner.cs | 66 + Scanners/ms17_010scanner.cs | 3 +- Scanners/nullsessionTrustScanner.cs | 80 + Tasks.cs | 1621 ++++++------ app.config | 2 +- changelog.txt | 31 + template/.gitignore | 2 + template/TemplateManager.cs | 35 + template/bootstrap.min.css.gz | Bin 0 -> 21023 bytes template/bootstrap.min.js.gz | Bin 0 -> 14057 bytes template/dataTables.bootstrap4.min.css.gz | Bin 0 -> 1010 bytes template/dataTables.bootstrap4.min.js.gz | Bin 0 -> 1120 bytes template/jquery.dataTables.min.js.gz | Bin 0 -> 27156 bytes template/jquery.min.js.gz | Bin 0 -> 29359 bytes template/popper.min.js.gz | Bin 0 -> 7292 bytes template/responsivetemplate.html.gz | Bin 62601 -> 342 bytes 199 files changed, 16284 insertions(+), 9157 deletions(-) rename {Healthcheck => ADWS}/DomainLocator.cs (99%) create mode 100644 ADWS/IADConnection.cs create mode 100644 ConsoleMenu.cs rename {Healthcheck => Data}/DomainKey.cs (78%) delete mode 100644 Data/HealthcheckDataCollection.cs create mode 100644 Data/IPingCastleReport.cs create mode 100644 Data/IPingCastleReportGenerator.cs create mode 100644 Data/PingCastleReportCollection.cs create mode 100644 Data/PingCastleReportDataExportLevel.cs rename Data/{HealthcheckDataHelper.cs => PingCastleReportHelper.cs} (78%) rename Data/{HealthcheckDataHistoryCollection.cs => PingCastleReportHistoryCollection.cs} (72%) create mode 100644 Graph/Reporting/GraphObjectReference.cs create mode 100644 Graph/Rules/CompromiseGraphAnomalyIndirectCritical10.cs create mode 100644 Graph/Rules/CompromiseGraphAnomalyIndirectCritical100.cs create mode 100644 Graph/Rules/CompromiseGraphAnomalyIndirectCritical50.cs create mode 100644 Graph/Rules/CompromiseGraphAnomalyIndirectCriticalAny.cs create mode 100644 Graph/Rules/CompromiseGraphAnomalyIndirectHigh10.cs create mode 100644 Graph/Rules/CompromiseGraphAnomalyIndirectHigh100.cs create mode 100644 Graph/Rules/CompromiseGraphAnomalyIndirectHigh50.cs create mode 100644 Graph/Rules/CompromiseGraphAnomalyIndirectHighAny.cs create mode 100644 Graph/Rules/CompromiseGraphAnomalyIndirectMedium10.cs create mode 100644 Graph/Rules/CompromiseGraphAnomalyIndirectMedium100.cs create mode 100644 Graph/Rules/CompromiseGraphAnomalyIndirectMedium50.cs create mode 100644 Graph/Rules/CompromiseGraphAnomalyIndirectMediumAny.cs create mode 100644 Graph/Rules/CompromiseGraphAnomalyIndirectUser.cs create mode 100644 Graph/Rules/CompromiseGraphPrivilegedOperatorsEmpty.cs create mode 100644 Graph/Rules/CompromiseGraphRule.cs create mode 100644 Graph/Rules/CompromiseGraphStalePermissionsCleanup.cs create mode 100644 Graph/Rules/CompromiseGraphTrustChildDomainHasPermission.cs create mode 100644 Graph/Rules/CompromiseGraphTrustMoreThanOneDomainControlCritical.cs create mode 100644 Graph/Rules/CompromiseGraphTrustOneDomainControlCritical.cs create mode 100644 Graph/Rules/CompromiseGraphTrustOneDomainControlUserDefined.cs create mode 100644 Graph/Rules/RuleDescription.resx delete mode 100644 Healthcheck/HealthcheckRules.cs create mode 100644 Healthcheck/Rules/HeatlcheckRuleAnomalyDCSpooler.cs create mode 100644 Healthcheck/Rules/HeatlcheckRuleAnomalyNotEnoughDC.cs delete mode 100644 Healthcheck/Rules/HeatlcheckRuleAttribute.cs create mode 100644 Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationGPOData.cs create mode 100644 Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationKeyAdmin.cs create mode 100644 Healthcheck/Rules/HeatlcheckRulePrivilegedExchangeAdminSDHolder.cs create mode 100644 Healthcheck/Rules/HeatlcheckRulePrivilegedOperatorsEmpty.cs create mode 100644 Healthcheck/Rules/HeatlcheckRulePrivilegedPrivilegeEveryone.cs create mode 100644 Healthcheck/Rules/HeatlcheckRulePrivilegedUnconstrainedDelegation.cs create mode 100644 PingCastleException.cs create mode 100644 PingCastleFactory.cs rename {NullSession => RPC}/lsa.cs (99%) rename {NullSession => RPC}/nativemethods.cs (86%) rename {NullSession => RPC}/nrpc.cs (98%) rename {NullSession => RPC}/nullsession.cs (99%) rename {NullSession => RPC}/rpcapi.cs (91%) rename {NullSession => RPC}/samr.cs (99%) create mode 100644 RPC/spool.cs delete mode 100644 Report/HealthCheckReportBase.cs delete mode 100644 Report/HealthCheckReportCompromiseGraph.cs delete mode 100644 Report/HealthCheckReportConsolidation.cs delete mode 100644 Report/HealthCheckReportSingle.cs create mode 100644 Report/IPingCastleReportUser.cs create mode 100644 Report/ReportBase.cs create mode 100644 Report/ReportCompromiseGraph.cs create mode 100644 Report/ReportCompromiseGraphConsolidation.cs create mode 100644 Report/ReportHealthCheckConsolidation.cs create mode 100644 Report/ReportHealthCheckSingle.cs create mode 100644 Report/ReportHelper.cs rename Report/{HealthCheckReportMapBuilder.cs => ReportMapBuilder.cs} (69%) create mode 100644 Report/ReportRiskControls.cs create mode 100644 Rules/IRiskEvaluation.cs create mode 100644 Rules/IRuleScore.cs create mode 100644 Rules/RiskModelCategory.cs create mode 100644 Rules/RiskModelObjective.cs create mode 100644 Rules/RiskRuleCategory.cs create mode 100644 Rules/RuleAttribute.cs rename Healthcheck/Rules/HeatlcheckRuleBase.cs => Rules/RuleBase.cs (62%) create mode 100644 Rules/RuleSet.cs create mode 100644 Scanners/ACLScanner.cs delete mode 100644 Scanners/AccountEnumeratorViaTrust.cs create mode 100644 Scanners/ConsistencyScanner.cs create mode 100644 Scanners/ForeignUsersScanner.cs create mode 100644 Scanners/NullSessionScanner.cs create mode 100644 Scanners/Smb1Protocol.cs create mode 100644 Scanners/Smb2Protocol.cs create mode 100644 Scanners/SpoolerScanner.cs create mode 100644 Scanners/nullsessionTrustScanner.cs create mode 100644 template/bootstrap.min.css.gz create mode 100644 template/bootstrap.min.js.gz create mode 100644 template/dataTables.bootstrap4.min.css.gz create mode 100644 template/dataTables.bootstrap4.min.js.gz create mode 100644 template/jquery.dataTables.min.js.gz create mode 100644 template/jquery.min.js.gz create mode 100644 template/popper.min.js.gz diff --git a/ADWS/ADConnection.cs b/ADWS/ADConnection.cs index 572722f..0e4e6c6 100644 --- a/ADWS/ADConnection.cs +++ b/ADWS/ADConnection.cs @@ -6,16 +6,16 @@ // using System; using System.Collections.Generic; +using System.Diagnostics; using System.Net; using System.Text; namespace PingCastle.ADWS { - public abstract class ADConnection + public abstract class ADConnection : IADConnection { public abstract void Enumerate(string distinguishedName, string filter, string[] properties, WorkOnReturnedObjectByADWS callback, string scope); - public abstract void EnumerateUsingWorkerThread(string distinguishedName, string filter, string[] properties, WorkOnReturnedObjectByADWS callback, string scope); public abstract void EstablishConnection(); public string Server { get; set; } @@ -52,10 +52,18 @@ public static string EscapeLDAP(string input) public static string EncodeSidToString(string sid) { - var realsid = new System.Security.Principal.SecurityIdentifier(sid); - var bytesid = new byte[realsid.BinaryLength]; - realsid.GetBinaryForm(bytesid, 0); - return "\\" + BitConverter.ToString(bytesid).Replace("-", "\\"); + try + { + var realsid = new System.Security.Principal.SecurityIdentifier(sid); + var bytesid = new byte[realsid.BinaryLength]; + realsid.GetBinaryForm(bytesid, 0); + return "\\" + BitConverter.ToString(bytesid).Replace("-", "\\"); + } + catch (ArgumentException) + { + Trace.WriteLine("Unable to encode " + sid); + throw; + } } } } diff --git a/ADWS/ADDomainInfo.cs b/ADWS/ADDomainInfo.cs index f485a50..ff40136 100644 --- a/ADWS/ADDomainInfo.cs +++ b/ADWS/ADDomainInfo.cs @@ -90,9 +90,10 @@ private static int ExtractIntValue(XmlNode item) public static ADDomainInfo Create(DirectoryEntry rootDSE) { ADDomainInfo info = new ADDomainInfo(); - info.DefaultNamingContext = rootDSE.Properties["defaultNamingContext"].Value as string; - info.ConfigurationNamingContext = rootDSE.Properties["configurationNamingContext"].Value as string; - info.DnsHostName = rootDSE.Properties["dnsHostName"].Value as string; + Trace.WriteLine("rootDse property count: " + rootDSE.Properties.Count); + info.DefaultNamingContext = rootDSE.Properties["defaultNamingContext"].Value as string; + info.ConfigurationNamingContext = rootDSE.Properties["configurationNamingContext"].Value as string; + info.DnsHostName = rootDSE.Properties["dnsHostName"].Value as string; if (rootDSE.Properties.Contains("domainFunctionality")) info.DomainFunctionality = int.Parse(rootDSE.Properties["domainFunctionality"].Value as string); if (rootDSE.Properties.Contains("forestFunctionality")) diff --git a/ADWS/ADItem.cs b/ADWS/ADItem.cs index d0be97f..72824fb 100644 --- a/ADWS/ADItem.cs +++ b/ADWS/ADItem.cs @@ -75,6 +75,7 @@ public class ReplPropertyMetaDataItem public byte[] SchemaInfo { get; set; } public string ScriptPath { get; set; } public SecurityIdentifier SecurityIdentifier { get; set; } + public string[] ServicePrincipalName { get; set; } public SecurityIdentifier[] SIDHistory { get; set; } public string[] SiteObjectBL { get; set; } public int TrustAttributes { get; set; } @@ -435,6 +436,9 @@ public static ADItem Create(XmlElement item) case "securityIdentifier": aditem.SecurityIdentifier = ExtractSIDValue(child); break; + case "servicePrincipalName": + aditem.ServicePrincipalName = ExtractStringArrayValue(child); + break; case "sIDHistory": aditem.SIDHistory = ExtractSIDArrayValue(child); break; @@ -646,6 +650,16 @@ public static ADItem Create(SearchResult sr, bool nTSecurityDescriptor) case "securityidentifier": aditem.SecurityIdentifier = new SecurityIdentifier((byte[])sr.Properties[name][0], 0); break; + case "serviceprincipalname": + { + List list = new List(); + foreach (string data in sr.Properties[name]) + { + list.Add(data); + } + aditem.ServicePrincipalName = list.ToArray(); + } + break; case "sidhistory": { List list = new List(); diff --git a/ADWS/ADWSConnection.cs b/ADWS/ADWSConnection.cs index f77d416..a9b197f 100644 --- a/ADWS/ADWSConnection.cs +++ b/ADWS/ADWSConnection.cs @@ -327,72 +327,24 @@ public override void Enumerate(string distinguishedName, string filter, string[] { foreach (XmlElement item in items.Any) { - ADItem aditem = ADItem.Create(item); - callback(aditem); - } - } - } - ); - } - - // translation securitydescriptor can take of lot of CPU - public override void EnumerateUsingWorkerThread(string distinguishedName, string filter, string[] properties, WorkOnReturnedObjectByADWS callback, string scope) - { - BlockingQueue queue = new BlockingQueue(600); - // background thread - Thread workingthread = null; - try - { - workingthread = new Thread( - () => - { - ItemListType items = null; - for (; ; ) - { - if (!queue.Dequeue(out items)) - break; - foreach (XmlElement item in items.Any) + ADItem aditem = null; + try { - ADItem aditem = ADItem.Create(item); - try - { - callback(aditem); - } - catch (Exception ex) - { - Trace.WriteLine("Exception in workerthread:" + ex.Message); - Trace.WriteLine(ex.StackTrace); - } + aditem = ADItem.Create(item); } + catch (Exception ex) + { + Console.WriteLine("Warning: unable to process element (" + ex.Message + ")\r\n" + item.OuterXml); + Trace.WriteLine("Warning: unable to process element\r\n" + item.OuterXml); + Trace.WriteLine("Exception: " + ex.Message); + Trace.WriteLine(ex.StackTrace); + } + if (aditem != null) + callback(aditem); } - Trace.WriteLine("Exiting worker thread"); - } - ); - - ReceiveItems callbackInternal = (ItemListType items) - => - { - if (items != null) - { - queue.Enqueue(items); } } - ; - - workingthread.Start(); - EnumerateInternalWithADWS(distinguishedName, filter, properties, scope, callbackInternal); - Trace.WriteLine("Done enumerating objects at: " + DateTime.Now); - queue.Quit(); - workingthread.Join(); - Trace.WriteLine("Enumeration using worker thread complete"); - } - finally - { - queue.Quit(); - if (workingthread != null) - if (workingthread.ThreadState == System.Threading.ThreadState.Running) - workingthread.Abort(); - } + ); } XmlQualifiedName[] BuildProperties(List properties) @@ -535,7 +487,7 @@ private void EnumerateInternalWithADWS(string distinguishedName, string filter, var stringValue = Convert.ToString(stringWriter); Trace.WriteLine("Detail:"); Trace.WriteLine(stringValue); - throw new ApplicationException("An ADWS exception occured (fault:" + ex.Message + ";reason:" + ex.Reason + ").\r\nADWS is a faster protocol than LDAP but bound to a default 30 minutes limitation. If this error persists, we recommand to force the LDAP protocol. Run PingCastle with the following switches: --protocol LDAPOnly --interactive"); + throw new PingCastleException("An ADWS exception occured (fault:" + ex.Message + ";reason:" + ex.Reason + ").\r\nADWS is a faster protocol than LDAP but bound to a default 30 minutes limitation. If this error persists, we recommand to force the LDAP protocol. Run PingCastle with the following switches: --protocol LDAPOnly --interactive"); } Trace.WriteLine("[" + DateTime.Now.ToLongTimeString() + "]Pull successful"); if (pullResponse.EndOfSequence != null) diff --git a/ADWS/ADWebService.cs b/ADWS/ADWebService.cs index 4665211..861e155 100644 --- a/ADWS/ADWebService.cs +++ b/ADWS/ADWebService.cs @@ -55,7 +55,8 @@ public ADWebService(string server, int port, NetworkCredential credential) public NetworkCredential Credential { get; set; } - private ADConnection connection { get; set; } + private IADConnection connection { get; set; } + private IADConnection fallBackConnection { get; set; } #region connection establishment private void EstablishConnection() @@ -91,6 +92,7 @@ private void EstablishConnection() adwsConnection.EstablishConnection(); Trace.WriteLine("ADWS connection successful"); connection = adwsConnection; + fallBackConnection = new LDAPConnection(adwsConnection.Server, adwsConnection.Port, Credential); } catch (Exception ex) { @@ -117,6 +119,7 @@ private void EstablishConnection() ldapConnection.EstablishConnection(); Trace.WriteLine("LDAP connection successful"); connection = ldapConnection; + fallBackConnection = new ADWSConnection(adwsConnection.Server, adwsConnection.Port, Credential); } catch (Exception ex) { @@ -232,17 +235,37 @@ public List BuildOUExplorationList(string OU, int NumberOfDepthFo public void Enumerate(string distinguishedName, string filter, string[] properties, WorkOnReturnedObjectByADWS callback) { - Enumerate(distinguishedName, filter, properties, callback, "Subtree"); + Enumerate(null, distinguishedName, filter, properties, callback, "Subtree"); } public void Enumerate(string distinguishedName, string filter, string[] properties, WorkOnReturnedObjectByADWS callback, string scope) { - connection.Enumerate(distinguishedName, filter, properties, callback, scope); + Enumerate(null, distinguishedName, filter, properties, callback, scope); } - public void EnumerateUsingWorkerThread(string distinguishedName, string filter, string[] properties, WorkOnReturnedObjectByADWS callback, string scope) + public delegate void Action(); + + public void Enumerate(Action preambleWithReentry, string distinguishedName, string filter, string[] properties, WorkOnReturnedObjectByADWS callback, string scope) { - connection.EnumerateUsingWorkerThread(distinguishedName, filter, properties, callback, scope); + if (preambleWithReentry != null) + preambleWithReentry(); + try + { + connection.Enumerate(distinguishedName, filter, properties, callback, scope); + } + catch (Exception ex) + { + Trace.WriteLine("Exception: " + ex.Message); + Trace.WriteLine("StackTrace: " + ex.StackTrace); + if (fallBackConnection == null) + throw; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("The AD query failed. Using the alternative protocol (" + fallBackConnection.GetType().Name + ")"); + Console.ResetColor(); + if (preambleWithReentry != null) + preambleWithReentry(); + fallBackConnection.Enumerate(distinguishedName, filter, properties, callback, scope); + } } #region IDispose diff --git a/Healthcheck/DomainLocator.cs b/ADWS/DomainLocator.cs similarity index 99% rename from Healthcheck/DomainLocator.cs rename to ADWS/DomainLocator.cs index fdbe56c..dd6e180 100644 --- a/Healthcheck/DomainLocator.cs +++ b/ADWS/DomainLocator.cs @@ -11,7 +11,7 @@ using System.Security.Permissions; using System.Text; -namespace PingCastle.Healthcheck +namespace PingCastle.ADWS { public class DomainLocator { diff --git a/ADWS/IADConnection.cs b/ADWS/IADConnection.cs new file mode 100644 index 0000000..aa026c4 --- /dev/null +++ b/ADWS/IADConnection.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace PingCastle.ADWS +{ + public interface IADConnection + { + ADDomainInfo GetDomainInfo(); + + void Enumerate(string distinguishedName, string filter, string[] properties, WorkOnReturnedObjectByADWS callback, string scope); + + } +} diff --git a/ADWS/LDAPConnection.cs b/ADWS/LDAPConnection.cs index 64e5fa8..2fc1283 100644 --- a/ADWS/LDAPConnection.cs +++ b/ADWS/LDAPConnection.cs @@ -28,12 +28,6 @@ public override void Enumerate(string distinguishedName, string filter, string[] EnumerateInternalWithLDAP(distinguishedName, filter, properties, scope, callback); } - public override void EnumerateUsingWorkerThread(string distinguishedName, string filter, string[] properties, WorkOnReturnedObjectByADWS callback, string scope) - { - EnumerateInternalWithLDAP(distinguishedName, filter, properties, scope, callback); - } - - [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)] private void EnumerateInternalWithLDAP(string distinguishedName, string filter, string[] properties, string scope, WorkOnReturnedObjectByADWS callback) { @@ -46,11 +40,11 @@ private void EnumerateInternalWithLDAP(string distinguishedName, string filter, { if (Credential == null) { - entry = new DirectoryEntry(@"LDAP://" + Server + "/" + distinguishedName, null, null, AuthenticationTypes.ServerBind | AuthenticationTypes.Secure); + entry = new DirectoryEntry(@"LDAP://" + Server + (Port == 0 ? null : ":" + Port) + "/" + distinguishedName, null, null, AuthenticationTypes.ServerBind | AuthenticationTypes.Secure | (Port == 636 ? AuthenticationTypes.SecureSocketsLayer:0)); } else { - entry = new DirectoryEntry(@"LDAP://" + Server + "/" + distinguishedName, Credential.UserName, Credential.Password, AuthenticationTypes.ServerBind | AuthenticationTypes.Secure); + entry = new DirectoryEntry(@"LDAP://" + Server + (Port == 0 ? null : ":" + Port) + "/" + distinguishedName, Credential.UserName, Credential.Password, AuthenticationTypes.ServerBind | AuthenticationTypes.Secure | (Port == 636 ? AuthenticationTypes.SecureSocketsLayer : 0)); } DirectorySearcher clsDS = new DirectorySearcher(entry); @@ -83,8 +77,20 @@ private void EnumerateInternalWithLDAP(string distinguishedName, string filter, Trace.WriteLine("[" + DateTime.Now.ToLongTimeString() + "]Calling FindAll"); foreach (SearchResult sr in clsDS.FindAll()) { - ADItem aditem = ADItem.Create(sr, nTSecurityDescriptor); - callback(aditem); + ADItem aditem = null; + try + { + aditem = ADItem.Create(sr, nTSecurityDescriptor); + } + catch (Exception ex) + { + Console.WriteLine("Warning: unable to process element (" + ex.Message + ")\r\n" + sr.Path); + Trace.WriteLine("Warning: unable to process element\r\n" + sr.Path); + Trace.WriteLine("Exception: " + ex.Message); + Trace.WriteLine(ex.StackTrace); + } + if (aditem != null) + callback(aditem); numberOfObjectAlreadyExtracted++; } Trace.WriteLine("[" + DateTime.Now.ToLongTimeString() + "]Enumeration successful"); @@ -114,6 +120,14 @@ protected override ADDomainInfo GetDomainInfoInternal() private ADDomainInfo GetLDAPDomainInfo() { DirectoryEntry rootDse = new DirectoryEntry("LDAP://" + Server + "/RootDSE"); + if (Credential == null) + { + rootDse = new DirectoryEntry(@"LDAP://" + Server + (Port == 0 ? null : ":" + Port) + "/RootDSE", null, null, AuthenticationTypes.ServerBind | AuthenticationTypes.Secure | (Port == 636 ? AuthenticationTypes.SecureSocketsLayer : 0)); + } + else + { + rootDse = new DirectoryEntry(@"LDAP://" + Server + (Port == 0 ? null : ":" + Port) + "/RootDSE", Credential.UserName, Credential.Password, AuthenticationTypes.ServerBind | AuthenticationTypes.Secure | (Port == 636 ? AuthenticationTypes.SecureSocketsLayer : 0)); + } return ADDomainInfo.Create(rootDse); } diff --git a/ConsoleMenu.cs b/ConsoleMenu.cs new file mode 100644 index 0000000..7f0dbfd --- /dev/null +++ b/ConsoleMenu.cs @@ -0,0 +1,321 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; + +namespace PingCastle +{ + public class ConsoleMenu + { + + public static string Header { get; set; } + public static string Title { get; set; } + public static string Notice { get; set; } + public static string Information { get; set; } + + static void printSelectMenuStyle0(List> items, int currentIndex, int top, int left) + { + bool hasDescription = false; + int largerChoice = 0; + for (int i = 0; i < items.Count; i++) + { + if (!String.IsNullOrEmpty(items[i].Value)) + hasDescription = true; + int l = items[i].Key.Length; + if (l > largerChoice) + largerChoice = l; + } + Console.SetCursorPosition(left, top); + for (int i = 0; i < items.Count; i++) + { + if (i == currentIndex - 1) + { + Console.BackgroundColor = ConsoleColor.Gray; + Console.ForegroundColor = ConsoleColor.Black; + } + Console.Write(" " + (char)(i < 9 ? i + '1' : i - 9 + 'a') + "-" + items[i].Key); + if (hasDescription) + { + int diff = largerChoice - items[i].Key.Length; + if (diff > 0) + Console.Write(new String(' ', diff)); + if (!String.IsNullOrEmpty(items[i].Value)) + Console.Write("-" + items[i].Value); + } + Console.WriteLine(); + Console.ResetColor(); + } + if (0 == currentIndex) + { + Console.BackgroundColor = ConsoleColor.Gray; + Console.ForegroundColor = ConsoleColor.Black; + } + Console.WriteLine(" 0-Exit"); + Console.ResetColor(); + } + + static void printSelectMenuStyle1(List> items, int currentIndex, int top, int left) + { + string description = null; + Console.SetCursorPosition(left, top); + string item; + int maxDescription = 0; + for (int i = 0; i < items.Count; i++) + { + if (i == currentIndex -1) + { + Console.BackgroundColor = ConsoleColor.Gray; + Console.ForegroundColor = ConsoleColor.Black; + description = items[i].Value; + } + if (!String.IsNullOrEmpty(items[i].Value) && maxDescription < items[i].Value.Length) + maxDescription = items[i].Value.Length; + + item = " " + (char)(i < 9 ? i + '1' : i - 9 + 'a') + "-" + items[i].Key; + Console.SetCursorPosition(left + (i < (items.Count + 1) / 2 ? 0 : Console.WindowWidth / 2), top + i + (i < (items.Count + 1) / 2 ? 0 : -(items.Count + 1) / 2)); + Console.Write(item + new string(' ',Console.WindowWidth / 2 - item.Length - 1)); + Console.ResetColor(); + } + if (0 == currentIndex) + { + Console.BackgroundColor = ConsoleColor.Gray; + Console.ForegroundColor = ConsoleColor.Black; + } + Console.SetCursorPosition(left, top + (items.Count+1) /2); + item = " 0-Exit"; + Console.WriteLine(item + new string(' ', Console.WindowWidth / 2 - item.Length - 1)); + Console.ResetColor(); + if (!String.IsNullOrEmpty(description)) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("=============================="); + Console.ResetColor(); + Console.WriteLine("Description:"); + int currentLineCursor = Console.CursorTop; + Console.WriteLine(new string(' ', maxDescription)); + Console.SetCursorPosition(0, currentLineCursor); + Console.WriteLine(description); + } + + } + + protected static void DisplayHeader() + { + Console.Clear(); + if (!String.IsNullOrEmpty(Header)) + { + Console.WriteLine(Header); + } + if (!String.IsNullOrEmpty(Title)) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine(Title); + Console.WriteLine(new string('=', Title.Length)); + Console.ResetColor(); + } + if (!String.IsNullOrEmpty(Information)) + { + Console.WriteLine(Information); + } + if (!String.IsNullOrEmpty(Notice)) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(Notice); + Console.ResetColor(); + } + } + + private static void ClearTopic() + { + Information = null; + Notice = null; + Title = null; + } + + public static string AskForString() + { + DisplayHeader(); + ClearTopic(); + return Console.ReadLine(); + } + + public static List AskForListString() + { + DisplayHeader(); + ClearTopic(); + var list = new List(); + var line = Console.ReadLine(); + while (!String.IsNullOrEmpty(line)) + { + list.Add(line); + line = Console.ReadLine(); + } + return list; + } + + public static int SelectMenu(List> items, int defaultIndex = 1) + { + DisplayHeader(); + ClearTopic(); + return SelectMenu(items, defaultIndex, 0); + } + + public static int SelectMenuCompact(List> items, int defaultIndex = 1) + { + DisplayHeader(); + ClearTopic(); + return SelectMenu(items, defaultIndex, 1); + } + + protected static int SelectMenu(List> items, int defaultIndex = 1, int style = 0) + { + int top = Console.CursorTop; + int left = Console.CursorLeft; + int index = defaultIndex; + Console.CursorVisible = false; + while (true) + { + switch (style) + { + case 1: + printSelectMenuStyle1(items, index, top, left); + break; + case 0: + default: + printSelectMenuStyle0(items, index, top, left); + break; + } + ConsoleKeyInfo ckey = Console.ReadKey(true); + + if (ckey.Key == ConsoleKey.Escape) + { + Console.CursorVisible = true; + Console.ResetColor(); + return 0; + } + if (ckey.Key == ConsoleKey.DownArrow) + { + if (index == items.Count) + { + index = 0; // exit key + } + else if (style == 1 && index == (items.Count + 1)/2) + { + index = 0; // exit key + } + else if (index == 0) + { + } + else { index++; } + } + else if (ckey.Key == ConsoleKey.UpArrow) + { + if (index == 1) + { + } + else if (index == 0) + { + if (style == 1) + { + index = (items.Count + 1)/ 2; + } + else + { + index = items.Count; + } + } + else { index--; } + } + else if (ckey.Key == ConsoleKey.LeftArrow && style == 1) + { + if (index >= (items.Count + 1) / 2) + { + index -= (items.Count + 1) / 2; + } + } + else if (ckey.Key == ConsoleKey.RightArrow && style == 1) + { + if (index <= (items.Count ) / 2) + { + index += (items.Count + 1) / 2; + } + } + else if (ckey.Key == ConsoleKey.Enter) + { + Console.CursorVisible = true; + Console.ResetColor(); + return index; + } + else + { + int number; + char key = ckey.KeyChar; + if (Int32.TryParse(key.ToString(), out number) && number >= 0 && number <= 9 && (number <= items.Count)) + { + Console.CursorVisible = true; + Console.ResetColor(); + return number; + } + if (key >= 'a' && key <= 'z' && ((key - 'a' + 10) <= items.Count)) + { + Console.CursorVisible = true; + Console.ResetColor(); + return (key - 'a' + 10); + } + if (key >= 'A' && key <= 'Z' && ((key - 'A' + 10) <= items.Count)) + { + Console.CursorVisible = true; + Console.ResetColor(); + return (key - 'A' + 10); + } + } + } + } + + // http://msdn.microsoft.com/en-us/library/ms680313 + +#pragma warning disable 0649 + struct _IMAGE_FILE_HEADER + { + public ushort Machine; + public ushort NumberOfSections; + public uint TimeDateStamp; + public uint PointerToSymbolTable; + public uint NumberOfSymbols; + public ushort SizeOfOptionalHeader; + public ushort Characteristics; + }; +#pragma warning restore 0649 + + public static DateTime GetBuildDateTime(Assembly assembly) + { + var path = assembly.Location; + if (File.Exists(path)) + { + var buffer = new byte[Marshal.SizeOf(typeof(_IMAGE_FILE_HEADER))]; + using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read)) + { + fileStream.Position = 0x3C; + fileStream.Read(buffer, 0, 4); + fileStream.Position = BitConverter.ToUInt32(buffer, 0); // COFF header offset + fileStream.Read(buffer, 0, 4); // "PE\0\0" + fileStream.Read(buffer, 0, buffer.Length); + } + var pinnedBuffer = GCHandle.Alloc(buffer, GCHandleType.Pinned); + try + { + var coffHeader = (_IMAGE_FILE_HEADER)Marshal.PtrToStructure(pinnedBuffer.AddrOfPinnedObject(), typeof(_IMAGE_FILE_HEADER)); + + return TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1) + new TimeSpan(coffHeader.TimeDateStamp * TimeSpan.TicksPerSecond)); + } + finally + { + pinnedBuffer.Free(); + } + } + return new DateTime(); + } + } +} diff --git a/Data/CompromiseGraphData.cs b/Data/CompromiseGraphData.cs index 0b7c85d..ec35993 100644 --- a/Data/CompromiseGraphData.cs +++ b/Data/CompromiseGraphData.cs @@ -5,22 +5,51 @@ // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. // using PingCastle.Healthcheck; +using PingCastle.Rules; using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.Text; using System.Xml.Serialization; +using PingCastle.ADWS; +using PingCastle.Graph.Database; +using System.Runtime.Serialization; namespace PingCastle.Data { + + public enum CompromiseGraphDataObjectRisk + { + Critical, + High, + Medium, + Other + } + [DebuggerDisplay("{Name}")] - public class CompromiseGraphData + public class CompromiseGraphData : IRiskEvaluationOnObjective, IPingCastleReport { public string EngineVersion { get; set; } public DateTime GenerationDate { get; set; } + public string GetHumanReadableFileName() + { + return "ad_cg_" + DomainFQDN + ".html"; + } + public string GetMachineReadableFileName() + { + return "ad_cg_" + DomainFQDN + ".xml"; + } + + public void SetExportLevel(PingCastleReportDataExportLevel level) + { + //Level = level; + } + public string DomainFQDN { get; set; } public string DomainSid { get; set; } + public string DomainNetBIOS { get; set; } private DomainKey _domain; [XmlIgnore] @@ -30,23 +59,226 @@ public DomainKey Domain { if (_domain == null) { - _domain = new DomainKey(DomainFQDN, DomainSid); + _domain = new DomainKey(DomainFQDN, DomainSid, DomainNetBIOS); } return _domain; } } public List Data { get; set; } + public List RiskRules { get; set; } + public List Dependancies { get; set; } + public List AnomalyAnalysis { get; set; } + + public int GlobalScore { get; set; } + public int StaleObjectsScore { get; set; } + public int PrivilegiedGroupScore { get; set; } + public int TrustScore { get; set; } + public int AnomalyScore { get; set; } + + [XmlIgnore] + public IList AllRiskRules { get { return RiskRules.ConvertAll(x => { return (IRuleScore)x; }); } } + [XmlIgnore] + public IList DomainKnown + { + get + { + var output = new List(); + output.Add(Domain); + foreach (var d in Dependancies) + { + output.Add(d.Domain); + } + return output; + } + } + } + + public class CompromiseGraphAnomalyAnalysisData + { + public CompromiseGraphDataObjectRisk ObjectRisk { get; set; } + public int NumberOfObjectsScreened { get; set; } + public int NumberOfObjectsWithIndirect {get;set;} + public int MaximumIndirectNumber { get; set; } + public int MaximumDirectIndirectRatio { get; set; } + public bool CriticalObjectFound { get; set; } + } + + public enum CompromiseGraphDataTypology + { + [Description("Admin groups")] + PrivilegedAccount = 10, + [Description("Critical Infrastructure")] + Infrastructure = 20, + [Description("User Defined")] + UserDefined = 100, + } + + public class CompromiseGraphRiskRule : IRuleScore + { + public CompromiseGraphRiskRule() + { + Level = PingCastleReportDataExportLevel.Full; + } + + [IgnoreDataMember] + [XmlIgnore] + public PingCastleReportDataExportLevel Level { get; set; } + + public int Points { get; set; } + public bool Achieved { get; set; } + + public RiskRuleCategory Category { get; set; } + + public RiskModelObjective Objective { get; set; } + + public string RiskId { get; set; } + + public string Rationale { get; set; } + + public bool ShouldSerializeDetails() { return (int)Level <= (int)PingCastleReportDataExportLevel.Full; } + public List Details { get; set; } + + public List ImpactedAssets { get; set; } + + } + + public class CompromiseGraphRiskRuleDetail + { + public string AssetName { get; set; } + public string Rationale { get; set; } + public List Details { get; set; } + } + + [DebuggerDisplay("{Name}")] + public class SingleCompromiseGraphUserMemberData + { + public string Name { get; set; } + + public string DistinguishedName { get; set; } + + public bool IsEnabled { get; set; } + + public bool IsActive { get; set; } + + public bool IsLocked { get; set; } + + public bool DoesPwdNeverExpires { get; set; } + + public bool CanBeDelegated { get; set; } + + public DateTime LastLogonTimestamp { get; set; } + + public DateTime PwdLastSet { get; set; } + + public bool SmartCardRequired { get; set; } + + public bool IsService { get; set; } + + public List SPN { get; set; } + + public bool IsPwdNotRequired { get; set; } + } + + [DebuggerDisplay("{Name}")] + public class SingleCompromiseGraphComputerMemberData + { + public string Name { get; set; } + + public string DistinguishedName { get; set; } + + public bool IsEnabled { get; set; } + + public bool IsActive { get; set; } + + public bool IsLocked { get; set; } + + public bool CanBeDelegated { get; set; } + + public DateTime LastLogonTimestamp { get; set; } + + public List SPN { get; set; } + } + + public class SingleCompromiseGraphIndirectMemberData + { + public string Name { get; set; } + public string Sid { get; set; } + public int Distance { get; set; } + public string AuthorizedObject { get; set; } + public string Path { get; set; } + } + + public class SingleCompromiseGraphDependancyMemberData + { + public string Name { get; set; } + public string Sid { get; set; } + } + + public class CompromiseGraphDependancyDetailData + { + public CompromiseGraphDataTypology Typology { get; set; } + public int NumberOfResolvedItems { get; set; } + public int NumberOfUnresolvedItems { get; set; } + public int NumberOfGroupImpacted { get; set; } + [XmlIgnore] + public List Items { get; set; } + } + + public class CompromiseGraphDependancyData + { + public string Netbios { get; set; } + public string FQDN { get; set; } + public string Sid { get; set; } + + public List Details { get; set; } + + private DomainKey _domain; + [XmlIgnore] + public DomainKey Domain + { + get + { + if (_domain == null) + { + _domain = new DomainKey(FQDN, Sid, Netbios); + } + return _domain; + } + } + } + + public class SingleCompromiseGraphDependancyData + { + public string Netbios { get; set; } + public string FQDN { get; set; } + public string Sid {get; set;} + public int NumberOfResolvedItems { get; set; } + public int NumberOfUnresolvedItems { get; set; } + public List Items { get; set; } + + } + + public class SingleCompromiseGraphDeletedData + { + public string Sid { get; set; } } public class SingleCompromiseGraphData { public string Name { get; set; } public string Description { get; set; } + public CompromiseGraphDataTypology Typology { get; set; } + public CompromiseGraphDataObjectRisk ObjectRisk { get; set; } public List Nodes { get; set; } public List Links { get; set; } public bool OnDemandAnalysis { get; set; } - public bool UnusualGroup { get; set; } + public bool CriticalObjectFound { get; set; } + public List DirectUserMembers { get; set; } + public List DirectComputerMembers { get; set; } + public List IndirectMembers { get; set; } + public List Dependancies { get; set; } + public List DeletedObjects { get; set; } } public class SingleCompromiseGraphNodeData @@ -56,6 +288,11 @@ public class SingleCompromiseGraphNodeData public string Type { get; set; } public string ShortName { get; set; } public int Distance {get; set; } + public bool Suspicious { get; set; } + public bool Critical { get; set; } + // used when building the structure + [XmlIgnore] + public ADItem ADItem { get; set; } } public class SingleCompromiseGraphLinkData diff --git a/Data/DataHelper.cs b/Data/DataHelper.cs index 304fabf..2b997c7 100644 --- a/Data/DataHelper.cs +++ b/Data/DataHelper.cs @@ -15,11 +15,14 @@ using System.Text; using System.Xml; using System.Xml.Serialization; +using PingCastle.Data; +using PingCastle.Healthcheck; +using PingCastle.Rules; -namespace PingCastle.Healthcheck +namespace PingCastle.Data { - public class DataHelper - { + public class DataHelper where T : IPingCastleReport + { // important: class to save xml string as UTF8 instead of UTF16 private sealed class Utf8StringWriter : StringWriter { @@ -33,9 +36,12 @@ public static string SaveAsXml(T data, string filename, bool EncryptReport) if (EncryptReport) { Utf8StringWriter w = new Utf8StringWriter(); - SaveAsXmlEncrypted(data, w); + SaveAsXmlEncrypted(data, w, HealthCheckEncryption.GetRSAEncryptionKey()); string xml = w.ToString(); - File.WriteAllText(filename, xml); + if (!string.IsNullOrEmpty(filename)) + { + File.WriteAllText(filename, xml); + } return xml; } else @@ -55,17 +61,20 @@ public static string SaveAsXml(T data, string filename, bool EncryptReport) private static string SaveAsXmlClearText(T data, string filename) { string xml = GetXmlClearText(data); - File.WriteAllText(filename, xml); + if (!string.IsNullOrEmpty(filename)) + { + File.WriteAllText(filename, xml); + } return xml; } public static string GetXmlClearText(T data) { - XmlSerializer xs = new XmlSerializer(typeof(T)); string xml = null; using (Utf8StringWriter wr = new Utf8StringWriter()) { - xs.Serialize(wr, data); + var xmlDoc = GetXmlDocumentClearText(data); + xmlDoc.Save(wr); xml = wr.ToString(); } return xml; @@ -84,7 +93,7 @@ public static XmlDocument GetXmlDocumentClearText(T data) return xmlDoc; } - private static void SaveAsXmlEncrypted(T data, TextWriter outStream) + public static void SaveAsXmlEncrypted(T data, TextWriter outStream, RSA rsaKey) { XmlDocument xmlDoc = GetXmlDocumentClearText(data); @@ -105,7 +114,7 @@ private static void SaveAsXmlEncrypted(T data, TextWriter outStream) edElement.EncryptionMethod = new EncryptionMethod(EncryptedXml.XmlEncAES256Url); EncryptedKey ek = new EncryptedKey(); - byte[] encryptedKey = EncryptedXml.EncryptKey(sessionKey.Key, HealthCheckEncryption.GetRSAEncryptionKey(), false); + byte[] encryptedKey = EncryptedXml.EncryptKey(sessionKey.Key, rsaKey, false); ek.CipherData = new CipherData(encryptedKey); ek.EncryptionMethod = new EncryptionMethod(EncryptedXml.XmlEncRSA15Url); @@ -182,11 +191,11 @@ private static void CheckForHCDataUnknownModel(HealthcheckData data) foreach (var rule in data.RiskRules) { // report was generated by an older version of PingCastle - if (rule.Model == HealthcheckRiskModelCategory.Unknown) + if (rule.Model == RiskModelCategory.Unknown) { - foreach (var r in HealthcheckRules.Rules) + foreach (var r in RuleSet.Rules) { - if (r.Id == rule.RiskId) + if (r.RiskId == rule.RiskId) { rule.Model = r.Model; break; diff --git a/Healthcheck/DomainKey.cs b/Data/DomainKey.cs similarity index 78% rename from Healthcheck/DomainKey.cs rename to Data/DomainKey.cs index dea4541..f5cb904 100644 --- a/Healthcheck/DomainKey.cs +++ b/Data/DomainKey.cs @@ -11,13 +11,15 @@ using System.Text; using System.Text.RegularExpressions; -namespace PingCastle.Healthcheck +namespace PingCastle.Data { [DebuggerDisplay("{DomainName} {DomainSID}")] public class DomainKey : IComparable, IEquatable { public string DomainName { get; set; } public string DomainSID { get; set; } + public string DomainNetBIOS { get; set; } + public bool IsComplete { get { return DomainSID != null && DomainName != null && DomainNetBIOS != null; } } private DomainKey() { @@ -25,23 +27,29 @@ private DomainKey() static Regex sidRegex = new Regex(@"(^$|^S-\d-(\d+-){1,14}\d+$)"); - public DomainKey(string DnsName, string domainSid) + public DomainKey(string DnsName, string domainSid, string domainNetbios) { - this.DomainName = DnsName; - if (!String.IsNullOrEmpty(this.DomainName)) - this.DomainName = this.DomainName.ToLowerInvariant(); - if (!String.IsNullOrEmpty(domainSid)) + + if (!string.IsNullOrEmpty(DnsName)) + { + // SID History data stored the SID in the FQDN field + if (domainSid != DnsName) + DomainName = DnsName.ToLowerInvariant(); + } + if (!string.IsNullOrEmpty(domainSid)) { if (sidRegex.IsMatch(domainSid)) { - this.DomainSID = domainSid; + DomainSID = domainSid; } else { Trace.WriteLine("Unable to parse the SID " + domainSid); - throw new ApplicationException("Unable to parse the SID \"" + domainSid + "\" - it should be like S-1-5-21-3777291851-731158365-1300944990"); + throw new PingCastleException("Unable to parse the SID \"" + domainSid + "\" - it should be like S-1-5-21-3777291851-731158365-1300944990"); } } + if (!string.IsNullOrEmpty(domainNetbios)) + DomainNetBIOS = domainNetbios.ToUpperInvariant(); } public override bool Equals(object obj) @@ -58,11 +66,11 @@ public override bool Equals(object obj) public bool Equals(DomainKey d) { - if (!String.IsNullOrEmpty(DomainSID) && !String.IsNullOrEmpty(d.DomainSID)) - return String.Equals(DomainSID, d.DomainSID, StringComparison.InvariantCultureIgnoreCase) ; + if (!string.IsNullOrEmpty(DomainSID) && !string.IsNullOrEmpty(d.DomainSID)) + return string.Equals(DomainSID, d.DomainSID, StringComparison.InvariantCultureIgnoreCase) ; // important: // if a SID is being associated to one domain, propagate this information - if (String.Equals(DomainName, d.DomainName, StringComparison.InvariantCultureIgnoreCase)) + if (string.Equals(DomainName, d.DomainName, StringComparison.InvariantCultureIgnoreCase)) { if (DomainSID == null && d.DomainSID != null) DomainSID = d.DomainSID; @@ -76,7 +84,7 @@ public bool Equals(DomainKey d) public static bool operator ==(DomainKey a, DomainKey b) { // If both are null, or both are same instance, return true. - if (System.Object.ReferenceEquals(a, b)) + if (Object.ReferenceEquals(a, b)) { return true; } diff --git a/Data/HealthCheckBUEntityData.cs b/Data/HealthCheckBUEntityData.cs index f02f09d..ca3bfca 100644 --- a/Data/HealthCheckBUEntityData.cs +++ b/Data/HealthCheckBUEntityData.cs @@ -4,6 +4,7 @@ // // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. // +using PingCastle.Data; using System; using System.Collections.Generic; using System.Diagnostics; diff --git a/Data/HealthcheckData.cs b/Data/HealthcheckData.cs index 09e9b98..ad32af7 100644 --- a/Data/HealthcheckData.cs +++ b/Data/HealthcheckData.cs @@ -8,9 +8,12 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; +using System.Runtime.Serialization; using System.Security.Principal; using System.Text; using System.Xml.Serialization; +using PingCastle.Data; +using PingCastle.Rules; namespace PingCastle.Healthcheck { @@ -39,6 +42,8 @@ public class HealthCheckGroupMemberData public DateTime PwdLastSet { get; set; } public bool SmartCardRequired { get; set; } + + public bool IsService { get; set; } } [DebuggerDisplay("{GroupName}")] @@ -46,11 +51,12 @@ public class HealthCheckGroupData { public HealthCheckGroupData() { - Level = HealthcheckDataLevel.Full; + Level = PingCastleReportDataExportLevel.Full; } - [XmlIgnore] - public HealthcheckDataLevel Level { get; set; } + [IgnoreDataMember] + [XmlIgnore] + public PingCastleReportDataExportLevel Level { get; set; } public string GroupName { get; set; } @@ -76,7 +82,9 @@ public HealthCheckGroupData() public int NumberOfSmartCardRequired { get; set; } - public bool ShouldSerializeMembers() { return (int)Level <= (int)HealthcheckDataLevel.Full; } + public int NumberOfServiceAccount { get; set; } + + public bool ShouldSerializeMembers() { return (int)Level <= (int)PingCastleReportDataExportLevel.Full; } public List Members { get; set; } } @@ -92,6 +100,7 @@ public class HealthCheckTrustDomainInfoData public string ForestNetbios { get; set; } private DomainKey _domain; + [IgnoreDataMember] [XmlIgnore] public DomainKey Domain { @@ -99,14 +108,15 @@ public DomainKey Domain { if (_domain == null) { - _domain = new DomainKey(DnsName, Sid); + _domain = new DomainKey(DnsName, Sid, NetbiosName); } return _domain; } } private DomainKey _forest; - [XmlIgnore] + [IgnoreDataMember] + [XmlIgnore] public DomainKey Forest { get @@ -116,7 +126,7 @@ public DomainKey Forest if (String.Equals(DnsName, ForestName, StringComparison.InvariantCultureIgnoreCase)) _forest = Domain; else - _forest = new DomainKey(ForestName, ForestSid); + _forest = new DomainKey(ForestName, ForestSid, ForestNetbios); } return _forest; } @@ -149,14 +159,15 @@ public class HealthCheckTrustData public List KnownDomains { get; set; } private DomainKey _domain; - [XmlIgnore] + [IgnoreDataMember] + [XmlIgnore] public DomainKey Domain { get { if (_domain == null) { - _domain = new DomainKey(TrustPartner, SID); + _domain = new DomainKey(TrustPartner, SID, NetBiosName); } return _domain; } @@ -243,7 +254,7 @@ public class HealthcheckAccountData { public HealthcheckAccountData() { - Level = HealthcheckDataLevel.Full; + Level = PingCastleReportDataExportLevel.Full; } public void Add(HealthcheckAccountData x) @@ -321,8 +332,9 @@ public void ClearProxy() ListTrustedToAuthenticateForDelegation = null; } - [XmlIgnore] - public HealthcheckDataLevel Level { get; set; } + [IgnoreDataMember] + [XmlIgnore] + public PingCastleReportDataExportLevel Level { get; set; } public int Number { get; set; } @@ -334,164 +346,88 @@ public void ClearProxy() public int NumberInactive { get; set; } - public bool ShouldSerializeListInactive() { return (int)Level <= (int)HealthcheckDataLevel.Full; } + public bool ShouldSerializeListInactive() { return (int)Level <= (int)PingCastleReportDataExportLevel.Full; } public List ListInactive { get; set; } public int NumberLocked { get; set; } - public bool ShouldSerializeListLocked() { return (int)Level <= (int)HealthcheckDataLevel.Full; } + public bool ShouldSerializeListLocked() { return (int)Level <= (int)PingCastleReportDataExportLevel.Full; } public List ListLocked { get; set; } public int NumberPwdNeverExpires { get; set; } - public bool ShouldSerializeListPwdNeverExpires() { return (int)Level <= (int)HealthcheckDataLevel.Full; } + public bool ShouldSerializeListPwdNeverExpires() { return (int)Level <= (int)PingCastleReportDataExportLevel.Full; } public List ListPwdNeverExpires { get; set; } public int NumberSidHistory { get; set; } - public bool ShouldSerializeListSidHistory() { return (int)Level <= (int)HealthcheckDataLevel.Full; } + public bool ShouldSerializeListSidHistory() { return (int)Level <= (int)PingCastleReportDataExportLevel.Full; } public List ListSidHistory { get; set; } - public bool ShouldSerializeListDomainSidHistory() { return (int)Level <= (int)HealthcheckDataLevel.Normal; } + public bool ShouldSerializeListDomainSidHistory() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } public List ListDomainSidHistory { get; set; } public int NumberPwdNotRequired { get; set; } - public bool ShouldSerializeListPwdNotRequired() { return (int)Level <= (int)HealthcheckDataLevel.Full; } + public bool ShouldSerializeListPwdNotRequired() { return (int)Level <= (int)PingCastleReportDataExportLevel.Full; } public List ListPwdNotRequired { get; set; } public int NumberBadPrimaryGroup { get; set; } - public bool ShouldSerializeListBadPrimaryGroup() { return (int)Level <= (int)HealthcheckDataLevel.Full; } + public bool ShouldSerializeListBadPrimaryGroup() { return (int)Level <= (int)PingCastleReportDataExportLevel.Full; } public List ListBadPrimaryGroup { get; set; } public int NumberDesEnabled { get; set; } - public bool ShouldSerializeListDesEnabled() { return (int)Level <= (int)HealthcheckDataLevel.Full; } + public bool ShouldSerializeListDesEnabled() { return (int)Level <= (int)PingCastleReportDataExportLevel.Full; } public List ListDesEnabled { get; set; } public int NumberTrustedToAuthenticateForDelegation { get; set; } - public bool ShouldSerializeListTrustedToAuthenticateForDelegation() { return (int)Level <= (int)HealthcheckDataLevel.Full; } + public bool ShouldSerializeListTrustedToAuthenticateForDelegation() { return (int)Level <= (int)PingCastleReportDataExportLevel.Full; } public List ListTrustedToAuthenticateForDelegation { get; set; } public int NumberReversibleEncryption { get; set; } - public bool ShouldSerializeListReversibleEncryption() { return (int)Level <= (int)HealthcheckDataLevel.Full; } + public bool ShouldSerializeListReversibleEncryption() { return (int)Level <= (int)PingCastleReportDataExportLevel.Full; } public List ListReversibleEncryption { get; set; } public int NumberDuplicate { get; set; } - public bool ShouldSerializeListDuplicate() { return (int)Level <= (int)HealthcheckDataLevel.Full; } + public bool ShouldSerializeListDuplicate() { return (int)Level <= (int)PingCastleReportDataExportLevel.Full; } public List ListDuplicate { get; set; } public int NumberNoPreAuth { get; set; } - public bool ShouldSerializeListNoPreAuth() { return (int)Level <= (int)HealthcheckDataLevel.Full; } + public bool ShouldSerializeListNoPreAuth() { return (int)Level <= (int)PingCastleReportDataExportLevel.Full; } public List ListNoPreAuth { get; set; } } - public enum HealthcheckRiskRuleCategory - { - StaleObjects, - PrivilegedAccounts, - Trusts, - Anomalies, - } - - public enum HealthcheckRiskModelCategory + public class HealthcheckRiskRule : IRuleScore { - Unknown = 0, - [Description("Inactive user or computer")] - InactiveUserOrComputer = 1000, - [Description("Vulnerability management")] - VulnerabilityManagement = 1001, - [Description("Replication")] - Replication = 1002, - [Description("Provisioning")] - Provisioning = 1003, - [Description("Old authentication protocols")] - OldAuthenticationProtocols = 1004, - [Description("Unfinished migration")] - UnfinishedMigration = 1005, - [Description("Obsolete OS")] - ObsoleteOS = 1006, - [Description("Object configuration")] - ObjectConfig = 1007, - [Description("Network topography")] - NetworkTopography = 1008, - [Description("Admin control")] - AdminControl = 2000, - [Description("Privilege control")] - PrivilegeControl = 2001, - [Description("ACL Check")] - ACLCheck = 2002, - [Description("Irreversible change")] - IrreversibleChange = 2003, - [Description("SID Filtering")] - SIDFiltering = 3000, - [Description("Trust inactive")] - TrustInactive = 3001, - [Description("Trust impermeability")] - TrustImpermeability = 3002, - [Description("SIDHistory")] - SIDHistory = 3003, - [Description("Old trust protocol")] - OldTrustProtocol = 3004, - [Description("Reconnaissance")] - Reconnaissance = 4000, - [Description("Local group vulnerability")] - LocalGroupVulnerability = 4001, - [Description("Password retrieval")] - PasswordRetrieval = 4002, - [Description("Weak password")] - WeakPassword = 4003, - [Description("Temporary admins")] - TemporaryAdmins = 4004, - [Description("Network sniffing")] - NetworkSniffing = 4005, - [Description("Certificate take over")] - CertificateTakeOver = 4006, - [Description("Pass-the-credential")] - PassTheCredential = 4007, - [Description("Golden ticket")] - GoldenTicket = 4008, - [Description("Backup")] - Backup = 4009, - } - - public class HealthcheckRiskRule - { public HealthcheckRiskRule() { - Level = HealthcheckDataLevel.Full; + Level = PingCastleReportDataExportLevel.Full; } - [XmlIgnore] - public HealthcheckDataLevel Level { get; set; } + [IgnoreDataMember] + [XmlIgnore] + public PingCastleReportDataExportLevel Level { get; set; } public int Points { get; set; } - public HealthcheckRiskRuleCategory Category { get; set; } + public RiskRuleCategory Category { get; set; } - public HealthcheckRiskModelCategory Model { get; set; } + public RiskModelCategory Model { get; set; } public string RiskId { get; set; } public string Rationale { get; set; } - public bool ShouldSerializeDetails() { return (int)Level <= (int)HealthcheckDataLevel.Full; } + public bool ShouldSerializeDetails() { return (int)Level <= (int)PingCastleReportDataExportLevel.Full; } public List Details { get; set; } } - public enum HealthcheckDataLevel - { - Full, - Normal, - Light, - Paranoid, - } - [DebuggerDisplay("{OperatingSystem}")] public class HealthcheckOSData { @@ -548,6 +484,8 @@ public class HealthcheckDelegationData public string Account { get; set; } + public string SecurityIdentifier { get; set; } + public string Right { get; set; } } @@ -559,6 +497,17 @@ public class HealthcheckScriptDelegationData public string Right { get; set; } } + [DebuggerDisplay("{GPOName} {Account} {Right}")] + public class GPODelegationData + { + public string GPOName { get; set; } + + public string Item { get; set; } + + public string Account { get; set; } + + public string Right { get; set; } + } //public class HealthcheckSiteTopologyData //{ @@ -578,19 +527,21 @@ public class HealthcheckSIDHistoryData { public string DomainSid { get; set; } public string FriendlyName { get; set; } + public string NetBIOSName { get; set; } public DateTime FirstDate { get; set; } public DateTime LastDate { get; set; } public int Count { get; set; } private DomainKey _domain; - [XmlIgnore] + [IgnoreDataMember] + [XmlIgnore] public DomainKey Domain { get { if (_domain == null) { - _domain = new DomainKey(FriendlyName,DomainSid); + _domain = new DomainKey(FriendlyName, DomainSid, NetBIOSName); } return _domain; } @@ -635,6 +586,8 @@ public class HealthcheckDomainController public SMBSecurityModeEnum SMB2SecurityMode { get; set; } + public bool RemoteSpoolerDetected { get; set; } + public List IP { get; set; } } @@ -648,14 +601,29 @@ public class HealthcheckSite } [DebuggerDisplay("{Domain}")] - public class HealthcheckData + public class HealthcheckData : IRiskEvaluation, IPingCastleReport { public string EngineVersion { get; set; } public DateTime GenerationDate { get; set; } + public string GetHumanReadableFileName() + { + return "ad_hc_" + DomainFQDN + ".html"; + } + + public string GetMachineReadableFileName() + { + return "ad_hc_" + DomainFQDN + ".xml"; + } + + public void SetExportLevel(PingCastleReportDataExportLevel level) + { + Level = level; + } + // this property is used to limit the serialization of some properties - private HealthcheckDataLevel _level; - public HealthcheckDataLevel Level + private PingCastleReportDataExportLevel _level; + public PingCastleReportDataExportLevel Level { get { @@ -709,135 +677,144 @@ public HealthcheckDataLevel Level public int TrustScore { get; set; } public int AnomalyScore { get; set; } - public bool ShouldSerializeTrusts() { return (int)Level <= (int)HealthcheckDataLevel.Light; } + public bool ShouldSerializeTrusts() { return (int)Level <= (int)PingCastleReportDataExportLevel.Light; } public List Trusts { get; set; } - public bool ShouldSerializeReachableDomains() { return (int)Level <= (int)HealthcheckDataLevel.Light; } + public bool ShouldSerializeReachableDomains() { return (int)Level <= (int)PingCastleReportDataExportLevel.Light; } public List ReachableDomains { get; set; } - public bool ShouldSerializeDomainControllers() { return (int)Level <= (int)HealthcheckDataLevel.Normal; } + public bool ShouldSerializeDomainControllers() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } public List DomainControllers { get; set; } - public bool ShouldSerializeSites() { return (int)Level <= (int)HealthcheckDataLevel.Normal; } + public bool ShouldSerializeSites() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } public List Sites { get; set; } - public bool ShouldSerializePreWindows2000AnonymousAccess() { return (int)Level <= (int)HealthcheckDataLevel.Normal; } + public bool ShouldSerializePreWindows2000AnonymousAccess() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } public bool PreWindows2000AnonymousAccess { get; set; } - public bool ShouldSerializeDsHeuristicsAnonymousAccess() { return (int)Level <= (int)HealthcheckDataLevel.Normal; } + public bool ShouldSerializeDsHeuristicsAnonymousAccess() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } public bool DsHeuristicsAnonymousAccess { get; set; } - public bool ShouldSerializeRiskRules() { return (int)Level <= (int)HealthcheckDataLevel.Light; } + public bool ShouldSerializeRiskRules() { return (int)Level <= (int)PingCastleReportDataExportLevel.Light; } public List RiskRules { get; set; } - public bool ShouldSerializeUserAccountData() { return (int)Level <= (int)HealthcheckDataLevel.Light; } + [IgnoreDataMember] + [XmlIgnore] + public IList AllRiskRules { get { return RiskRules.ConvertAll(x => { return (IRuleScore)x; }); } } + + public bool ShouldSerializeUserAccountData() { return (int)Level <= (int)PingCastleReportDataExportLevel.Light; } public HealthcheckAccountData UserAccountData { get; set; } - public bool ShouldSerializeComputerAccountData() { return (int)Level <= (int)HealthcheckDataLevel.Light; } + public bool ShouldSerializeComputerAccountData() { return (int)Level <= (int)PingCastleReportDataExportLevel.Light; } public HealthcheckAccountData ComputerAccountData { get; set; } - public bool ShouldSerializeOperatingSystem() { return (int)Level <= (int)HealthcheckDataLevel.Light; } + public bool ShouldSerializeOperatingSystem() { return (int)Level <= (int)PingCastleReportDataExportLevel.Light; } public List OperatingSystem { get; set; } // DO NOT USE - former data - public bool ShouldSerializeOperatingSystemDC() { return (int)Level <= (int)HealthcheckDataLevel.Light; } + public bool ShouldSerializeOperatingSystemDC() { return (int)Level <= (int)PingCastleReportDataExportLevel.Light; } public List OperatingSystemDC { get; set; } - public bool ShouldSerializeLoginScript() { return (int)Level <= (int)HealthcheckDataLevel.Normal; } + public bool ShouldSerializeLoginScript() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } public List LoginScript { get; set; } - public bool ShouldSerializeLastADBackup() { return (int)Level <= (int)HealthcheckDataLevel.Normal; } + public bool ShouldSerializeLastADBackup() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } public DateTime LastADBackup { get; set; } - public bool ShouldSerializeLAPSInstalled() { return (int)Level <= (int)HealthcheckDataLevel.Normal; } + public bool ShouldSerializeLAPSInstalled() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } public DateTime LAPSInstalled { get; set; } - public bool ShouldSerializeKrbtgtLastChangeDate() { return (int)Level <= (int)HealthcheckDataLevel.Light; } + public bool ShouldSerializeKrbtgtLastChangeDate() { return (int)Level <= (int)PingCastleReportDataExportLevel.Light; } public DateTime KrbtgtLastChangeDate { get; set; } - public bool ShouldSerializeKrbtgtLastVersion() { return (int)Level <= (int)HealthcheckDataLevel.Light; } + public bool ShouldSerializeKrbtgtLastVersion() { return (int)Level <= (int)PingCastleReportDataExportLevel.Light; } public int KrbtgtLastVersion { get; set; } - public bool ShouldSerializeAdminLastLoginDate() { return (int)Level <= (int)HealthcheckDataLevel.Normal; } + public bool ShouldSerializeAdminLastLoginDate() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } public DateTime AdminLastLoginDate { get; set; } - public bool ShouldSerializeAdminAccountName() { return (int)Level <= (int)HealthcheckDataLevel.Full; } + public bool ShouldSerializeAdminAccountName() { return (int)Level <= (int)PingCastleReportDataExportLevel.Full; } public string AdminAccountName { get; set; } - public bool ShouldSerializeGPPPassword() { return (int)Level <= (int)HealthcheckDataLevel.Normal; } + public bool ShouldSerializeGPPPassword() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } public List GPPPassword { get; set; } - public bool ShouldSerializeGPPRightAssignment() { return (int)Level <= (int)HealthcheckDataLevel.Full; } + public bool ShouldSerializeGPPRightAssignment() { return (int)Level <= (int)PingCastleReportDataExportLevel.Full; } public List GPPRightAssignment { get; set; } - public bool ShouldSerializeGPPPasswordPolicy() { return (int)Level <= (int)HealthcheckDataLevel.Normal; } + public bool ShouldSerializeGPPPasswordPolicy() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } public List GPPPasswordPolicy { get; set; } - public bool ShouldSerializeGPOLsaPolicy() { return (int)Level <= (int)HealthcheckDataLevel.Normal; } + public bool ShouldSerializeGPOLsaPolicy() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } public List GPOLsaPolicy { get; set; } - public bool ShouldSerializeGPOScreenSaverPolicy() { return (int)Level <= (int)HealthcheckDataLevel.Normal; } + public bool ShouldSerializeGPOScreenSaverPolicy() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } public List GPOScreenSaverPolicy { get; set; } - public bool ShouldSerializeGPOEventForwarding() { return (int)Level <= (int)HealthcheckDataLevel.Normal; } + public bool ShouldSerializeGPOEventForwarding() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } public List GPOEventForwarding { get; set; } - public bool ShouldSerializeGPOLocalMembership() { return (int)Level <= (int)HealthcheckDataLevel.Full; } + public bool ShouldSerializeGPODelegation() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } + public List GPODelegation { get; set; } + + public bool ShouldSerializeGPOLocalMembership() { return (int)Level <= (int)PingCastleReportDataExportLevel.Full; } public List GPOLocalMembership { get; set; } - public bool ShouldSerializeTrustedCertificates() { return (int)Level <= (int)HealthcheckDataLevel.Normal; } + public bool ShouldSerializeTrustedCertificates() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } public List TrustedCertificates { get; set; } - public bool ShouldSerializePrivilegedGroups() { return (int)Level <= (int)HealthcheckDataLevel.Light; } + public bool ShouldSerializePrivilegedGroups() { return (int)Level <= (int)PingCastleReportDataExportLevel.Light; } public List PrivilegedGroups { get; set; } - public bool ShouldSerializeAllPrivilegedMembers() { return (int)Level <= (int)HealthcheckDataLevel.Full; } + public bool ShouldSerializeAllPrivilegedMembers() { return (int)Level <= (int)PingCastleReportDataExportLevel.Full; } public List AllPrivilegedMembers { get; set; } - public bool ShouldSerializeAdminSDHolderNotOKCount() { return (int)Level <= (int)HealthcheckDataLevel.Normal; } + public bool ShouldSerializeAdminSDHolderNotOKCount() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } public int AdminSDHolderNotOKCount { get; set; } - public bool ShouldSerializeAdminSDHolderNotOK() { return (int)Level <= (int)HealthcheckDataLevel.Full; } + public bool ShouldSerializeAdminSDHolderNotOK() { return (int)Level <= (int)PingCastleReportDataExportLevel.Full; } public List AdminSDHolderNotOK { get; set; } - public bool ShouldSerializeSmartCardNotOKCount() { return (int)Level <= (int)HealthcheckDataLevel.Normal; } + public bool ShouldSerializeSmartCardNotOKCount() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } public int SmartCardNotOKCount { get; set; } - public bool ShouldSerializeSmartCardNotOK() { return (int)Level <= (int)HealthcheckDataLevel.Full; } + public bool ShouldSerializeSmartCardNotOK() { return (int)Level <= (int)PingCastleReportDataExportLevel.Full; } public List SmartCardNotOK { get; set; } - public bool ShouldSerializeDelegations() { return (int)Level <= (int)HealthcheckDataLevel.Full; } + public bool ShouldSerializeDelegations() { return (int)Level <= (int)PingCastleReportDataExportLevel.Full; } public List Delegations { get; set; } - public bool ShouldSerializeGPOLoginScript() { return (int)Level <= (int)HealthcheckDataLevel.Full; } + public bool ShouldSerializeGPOLoginScript() { return (int)Level <= (int)PingCastleReportDataExportLevel.Full; } public List GPOLoginScript { get; set; } //public bool ShouldSerializeSiteTopology() { return (int)Level <= (int)HealthcheckDataLevel.Normal; } //public List SiteTopology { get; set; } - public bool ShouldSerializeDomainControllerWithNullSessionCount() { return (int)Level <= (int)HealthcheckDataLevel.Normal; } + public bool ShouldSerializeDomainControllerWithNullSessionCount() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } public int DomainControllerWithNullSessionCount { get; set; } - public bool ShouldSerializeSIDHistoryAuditingGroupPresent() { return (int)Level <= (int)HealthcheckDataLevel.Normal; } + public bool ShouldSerializeSIDHistoryAuditingGroupPresent() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } public bool SIDHistoryAuditingGroupPresent { get; set; } - public bool ShouldSerializeMachineAccountQuota() { return (int)Level <= (int)HealthcheckDataLevel.Normal; } + public bool ShouldSerializeMachineAccountQuota() { return (int)Level <= (int)PingCastleReportDataExportLevel.Normal; } public int MachineAccountQuota { get; set; } private DomainKey _domain; - [XmlIgnore] + [IgnoreDataMember] + [XmlIgnore] public DomainKey Domain { get { if (_domain == null) { - _domain = new DomainKey(DomainFQDN, DomainSid); + _domain = new DomainKey(DomainFQDN, DomainSid, NetBIOSName); } return _domain; } } private DomainKey _forest; - [XmlIgnore] + [IgnoreDataMember] + [XmlIgnore] public DomainKey Forest { get @@ -851,6 +828,7 @@ public DomainKey Forest else { string sid = null; + string netbiosname = null; if (Trusts != null) { foreach (var trust in Trusts) @@ -860,16 +838,54 @@ public DomainKey Forest if (!String.IsNullOrEmpty(trust.SID)) { sid = trust.SID; + netbiosname = trust.NetBiosName; } break; } } } - _forest = new DomainKey(ForestFQDN, sid); + _forest = new DomainKey(ForestFQDN, sid, netbiosname); } } return _forest; } } + + [IgnoreDataMember] + [XmlIgnore] + public IList DomainKnown + { + get + { + var output = new List(); + output.Add(Domain); + if (Domain.DomainName != Forest.DomainName) + output.Add(Forest); + if (Trusts != null) + { + foreach (var t in Trusts) + { + output.Add(t.Domain); + if (t.KnownDomains != null) + { + foreach (var d in t.KnownDomains) + { + output.Add(d.Domain); + output.Add(d.Forest); + } + } + } + } + if (ReachableDomains != null) + { + foreach (var d in ReachableDomains) + { + output.Add(d.Domain); + output.Add(d.Forest); + } + } + return output; + } + } } } diff --git a/Data/HealthcheckDataCollection.cs b/Data/HealthcheckDataCollection.cs deleted file mode 100644 index fc2cfd4..0000000 --- a/Data/HealthcheckDataCollection.cs +++ /dev/null @@ -1,134 +0,0 @@ -// -// Copyright (c) Ping Castle. All rights reserved. -// https://www.pingcastle.com -// -// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. -// -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Security.Principal; -using System.Text; - -namespace PingCastle.Healthcheck -{ - - public class HealthcheckDataCollection : ICollection - { - Dictionary data { get; set; } - Dictionary> AmbigiousNameReference { get; set; } - - public HealthcheckDataCollection() - { - data = new Dictionary(); - AmbigiousNameReference = new Dictionary>(); - } - - public void Add(HealthcheckData item) - { - // taking the more recent report - var sid = item.DomainSid; - if (data.ContainsKey(sid)) - { - if (data[sid].GenerationDate < item.GenerationDate) - { - data[sid] = item; - } - } - else - { - data.Add(sid, item); - } - UpdateAmbigiousReference(item.Domain); - foreach(var t in item.Trusts) - { - UpdateAmbigiousReference(t.Domain); - foreach(var d in t.KnownDomains) - { - UpdateAmbigiousReference(d.Domain); - UpdateAmbigiousReference(d.Forest); - } - } - foreach(var d in item.ReachableDomains) - { - UpdateAmbigiousReference(d.Domain); - UpdateAmbigiousReference(d.Forest); - } - } - - private void UpdateAmbigiousReference(DomainKey domain) - { - if (domain == null || String.IsNullOrEmpty(domain.DomainSID) || String.IsNullOrEmpty(domain.DomainName)) - return; - if (!AmbigiousNameReference.ContainsKey(domain.DomainName)) - AmbigiousNameReference[domain.DomainName] = new List { domain.DomainSID }; - else if (!String.IsNullOrEmpty(domain.DomainSID)) - if (!AmbigiousNameReference[domain.DomainName].Contains(domain.DomainSID)) - AmbigiousNameReference[domain.DomainName].Add(domain.DomainSID); - } - - public void Clear() - { - data.Clear(); - } - - public bool Contains(HealthcheckData item) - { - return data.ContainsValue(item); - } - - public void CopyTo(HealthcheckData[] array, int arrayIndex) - { - data.Values.CopyTo(array, arrayIndex); - } - - public int Count - { - get { return data.Count; } - } - - public bool IsReadOnly - { - get { return false; } - } - - public bool Remove(HealthcheckData item) - { - return data.Remove(item.DomainSid); - } - - public IEnumerator GetEnumerator() - { - return data.Values.GetEnumerator(); - } - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() - { - return data.Values.GetEnumerator(); - } - - public HealthcheckData GetDomain(DomainKey key) - { - if (key.DomainSID != null) - { - if (data.ContainsKey(key.DomainSID)) - return data[key.DomainSID]; - return null; - } - foreach (HealthcheckData hc in data.Values) - { - if (String.Equals(hc.DomainFQDN, key.DomainName, StringComparison.InvariantCultureIgnoreCase)) - return hc; - } - return null; - } - - public bool HasDomainAmbigiousName(DomainKey domainKey) - { - if (AmbigiousNameReference.ContainsKey(domainKey.DomainName)) - return AmbigiousNameReference[domainKey.DomainName].Count > 1; - return true; - } - - } -} diff --git a/Data/IPingCastleReport.cs b/Data/IPingCastleReport.cs new file mode 100644 index 0000000..2939249 --- /dev/null +++ b/Data/IPingCastleReport.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Text; + +namespace PingCastle.Data +{ + public interface IPingCastleReport + { + DomainKey Domain { get; } + DateTime GenerationDate { get; } + string EngineVersion { get; } + // used to get trusted domain and detect ubiquous name (ex: corp.local) + IList DomainKnown {get;} + string GetHumanReadableFileName(); + string GetMachineReadableFileName(); + void SetExportLevel(PingCastleReportDataExportLevel level); + } +} diff --git a/Data/IPingCastleReportGenerator.cs b/Data/IPingCastleReportGenerator.cs new file mode 100644 index 0000000..c2eddd8 --- /dev/null +++ b/Data/IPingCastleReportGenerator.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text; + +namespace PingCastle.Data +{ + + public class PingCastleAnalyzerParameters + { + public string Server {get;set;} + public int Port {get;set;} + public NetworkCredential Credential { get; set; } + public bool PerformExtendedTrustDiscovery { get; set; } + public List AdditionalNamesForDelegationAnalysis { get; set; } + } + + public interface IPingCastleAnalyzer where T : IPingCastleReport + { + T PerformAnalyze(PingCastleAnalyzerParameters parameters); + } +} diff --git a/Data/PingCastleReportCollection.cs b/Data/PingCastleReportCollection.cs new file mode 100644 index 0000000..296a966 --- /dev/null +++ b/Data/PingCastleReportCollection.cs @@ -0,0 +1,199 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using PingCastle.Data; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Security.Principal; +using System.Text; + +namespace PingCastle.Data +{ + + public class PingCastleReportCollection : ICollection where T : IPingCastleReport + { + Dictionary data { get; set; } + // database of FQDN, all SID + Dictionary> AmbigiousNameReference { get; set; } + Dictionary DomainReference { get; set; } + + public PingCastleReportCollection() + { + data = new Dictionary(); + AmbigiousNameReference = new Dictionary>(); + DomainReference = new Dictionary(); + } + + public void Add(T item) + { + if (item == null) + return; + // taking the more recent report + var sid = item.Domain.DomainSID; + if (data.ContainsKey(sid)) + { + if (data[sid].GenerationDate < item.GenerationDate) + { + data[sid] = item; + } + } + else + { + data.Add(sid, item); + } + + } + + public void EnrichInformation() + { + // complete domain information + // indeed, data can be FQDN only (trust with remote deleted) + // NetBIOS + SID (SID History and unable to locate a server) + // SID Only (SID History from removed domain) + AmbigiousNameReference.Clear(); + + var sidreference = new Dictionary(); + + // first pass - build reference based on SID + foreach (var item in data.Values) + { + foreach (var domainKey in item.DomainKnown) + { + if (domainKey.IsComplete && !sidreference.ContainsKey(domainKey.DomainSID)) + { + sidreference.Add(domainKey.DomainSID, domainKey); + } + } + } + // second pass, build AmbigiousNameReference + foreach (var domain in sidreference.Values) + { + if (!AmbigiousNameReference.ContainsKey(domain.DomainName)) + AmbigiousNameReference[domain.DomainName] = new List { domain.DomainSID }; + else if (!string.IsNullOrEmpty(domain.DomainSID)) + if (!AmbigiousNameReference[domain.DomainName].Contains(domain.DomainSID)) + AmbigiousNameReference[domain.DomainName].Add(domain.DomainSID); + } + // third pass, update incomplete information based on the information we have + foreach (var item in data.Values) + { + foreach (var domainKey in item.DomainKnown) + { + if (domainKey.IsComplete) + { + continue; + } + // try to complete based on sid + if (!String.IsNullOrEmpty(domainKey.DomainSID) && sidreference.ContainsKey(domainKey.DomainSID)) + { + var reference = sidreference[domainKey.DomainSID]; + if (string.IsNullOrEmpty(domainKey.DomainNetBIOS)) + domainKey.DomainNetBIOS = reference.DomainNetBIOS; + if (string.IsNullOrEmpty(domainKey.DomainName)) + domainKey.DomainName = reference.DomainName; + } + else if (!String.IsNullOrEmpty(domainKey.DomainName)) + { + foreach (var reference in sidreference.Values) + { + if (reference.DomainName == domainKey.DomainName) + { + if (string.IsNullOrEmpty(domainKey.DomainNetBIOS)) + domainKey.DomainNetBIOS = reference.DomainNetBIOS; + break; + } + } + } + else if (!String.IsNullOrEmpty(domainKey.DomainNetBIOS)) + { + foreach (var reference in sidreference.Values) + { + if (reference.DomainNetBIOS == domainKey.DomainNetBIOS) + { + if (string.IsNullOrEmpty(domainKey.DomainName)) + { + domainKey.DomainName = reference.DomainName; + } + if (string.IsNullOrEmpty(domainKey.DomainName)) + domainKey.DomainName = reference.DomainName; + break; + } + } + } + } + } + } + + public void Clear() + { + data.Clear(); + } + + public bool Contains(T item) + { + return data.ContainsValue(item); + } + + public void CopyTo(T[] array, int arrayIndex) + { + data.Values.CopyTo(array, arrayIndex); + } + + public int Count + { + get { return data.Count; } + } + + public bool IsReadOnly + { + get { return false; } + } + + public bool Remove(T item) + { + if (item == null) + return false; + return data.Remove(item.Domain.DomainSID); + } + + public IEnumerator GetEnumerator() + { + return data.Values.GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return data.Values.GetEnumerator(); + } + + public T GetDomain(DomainKey key) + { + if (key == null) + return default(T); + if (key.DomainSID != null) + { + if (data.ContainsKey(key.DomainSID)) + return data[key.DomainSID]; + return default(T); + } + foreach (T hc in data.Values) + { + if (string.Equals(hc.Domain.DomainName, key.DomainName, StringComparison.InvariantCultureIgnoreCase)) + return hc; + } + return default(T); + } + + public bool HasDomainAmbigiousName(DomainKey domainKey) + { + if (AmbigiousNameReference.ContainsKey(domainKey.DomainName)) + return AmbigiousNameReference[domainKey.DomainName].Count > 1; + return true; + } + + } +} diff --git a/Data/PingCastleReportDataExportLevel.cs b/Data/PingCastleReportDataExportLevel.cs new file mode 100644 index 0000000..92111b7 --- /dev/null +++ b/Data/PingCastleReportDataExportLevel.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace PingCastle.Data +{ + public enum PingCastleReportDataExportLevel + { + Full, + Normal, + Light, + Paranoid, + } +} diff --git a/Data/HealthcheckDataHelper.cs b/Data/PingCastleReportHelper.cs similarity index 78% rename from Data/HealthcheckDataHelper.cs rename to Data/PingCastleReportHelper.cs index 1346012..35aa0db 100644 --- a/Data/HealthcheckDataHelper.cs +++ b/Data/PingCastleReportHelper.cs @@ -13,25 +13,25 @@ namespace PingCastle.Data { - public class HealthcheckDataHelper + public class PingCastleReportHelper where T : IPingCastleReport { - public static HealthcheckDataCollection LoadXmls(string Xmls, DateTime maxfiltervalue) + public static PingCastleReportCollection LoadXmls(string Xmls, DateTime maxfiltervalue) { - HealthcheckDataCollection output = new HealthcheckDataCollection(); + var output = new PingCastleReportCollection(); int files = 0; - foreach (string filename in Directory.GetFiles(Xmls, "*.xml", SearchOption.AllDirectories)) + foreach (string filename in Directory.GetFiles(Xmls, PingCastleFactory.GetFilePatternForLoad(), SearchOption.AllDirectories)) { try { files++; - HealthcheckData healthcheckData = DataHelper.LoadXml(filename); + T data = DataHelper.LoadXml(filename); // taking the more recent report - if (healthcheckData.GenerationDate > maxfiltervalue) + if (data.GenerationDate > maxfiltervalue) { - Trace.WriteLine("File " + filename + " ignored because generation date " + healthcheckData.GenerationDate.ToString("u") + " is after the consolidation date " + maxfiltervalue.ToString("u")); + Trace.WriteLine("File " + filename + " ignored because generation date " + data.GenerationDate.ToString("u") + " is after the consolidation date " + maxfiltervalue.ToString("u")); continue; } - output.Add(healthcheckData); + output.Add(data); } catch (Exception ex) @@ -44,26 +44,27 @@ public static HealthcheckDataCollection LoadXmls(string Xmls, DateTime maxfilter } } Console.WriteLine("Reports loaded: " + output.Count + " - on a total of " + files + " valid files"); + output.EnrichInformation(); return output; } - public static HealthcheckDataHistoryCollection LoadHistory(string Xmls, DateTime maxfiltervalue) + public static PingCastleReportHistoryCollection LoadHistory(string Xmls, DateTime maxfiltervalue) { - var output = new HealthcheckDataHistoryCollection(); + var output = new PingCastleReportHistoryCollection(); int files = 0; - foreach (string filename in Directory.GetFiles(Xmls, "*.xml", SearchOption.AllDirectories)) + foreach (string filename in Directory.GetFiles(Xmls, "*ad_hc_*.xml", SearchOption.AllDirectories)) { try { files++; - HealthcheckData healthcheckData = DataHelper.LoadXml(filename); + var data = DataHelper.LoadXml(filename); // taking the more recent report - if (healthcheckData.GenerationDate > maxfiltervalue) + if (data.GenerationDate > maxfiltervalue) { - Trace.WriteLine("File " + filename + " ignored because generation date " + healthcheckData.GenerationDate.ToString("u") + " is after the consolidation date " + maxfiltervalue.ToString("u")); + Trace.WriteLine("File " + filename + " ignored because generation date " + data.GenerationDate.ToString("u") + " is after the consolidation date " + maxfiltervalue.ToString("u")); continue; } - output.Add(healthcheckData); + output.Add(data); } catch (Exception ex) @@ -79,11 +80,11 @@ public static HealthcheckDataHistoryCollection LoadHistory(string Xmls, DateTime return output; } - public static HealthcheckDataCollection TransformReportsToDemo(HealthcheckDataCollection consolidation) + public static PingCastleReportCollection TransformReportsToDemo(PingCastleReportCollection consolidation) { string rotKey = GenerateRandomRotKey(); - HealthcheckDataCollection output = new HealthcheckDataCollection(); + var output = new PingCastleReportCollection(); foreach (HealthcheckData data in consolidation) { HealthcheckData demoreport = TransformReportToDemo(rotKey, data); diff --git a/Data/HealthcheckDataHistoryCollection.cs b/Data/PingCastleReportHistoryCollection.cs similarity index 72% rename from Data/HealthcheckDataHistoryCollection.cs rename to Data/PingCastleReportHistoryCollection.cs index 4257834..2b4274b 100644 --- a/Data/HealthcheckDataHistoryCollection.cs +++ b/Data/PingCastleReportHistoryCollection.cs @@ -4,30 +4,29 @@ // // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. // +using PingCastle.Data; using System; using System.Collections.Generic; -using System.Security.Principal; -using System.Text; namespace PingCastle.Healthcheck { - public class HealthcheckDataHistoryCollection : ICollection - { + public class PingCastleReportHistoryCollection : ICollection where T : IPingCastleReport + { - Dictionary> data { get; set; } + Dictionary> data { get; set; } - public HealthcheckDataHistoryCollection() + public PingCastleReportHistoryCollection() { - data = new Dictionary>(); + data = new Dictionary>(); } - public void Add(HealthcheckData item) + public void Add(T item) { // taking the more recent report - var sid = item.DomainSid; + var sid = item.Domain.DomainSID; if (!data.ContainsKey(sid)) { - data[sid] = new Dictionary(); + data[sid] = new Dictionary(); } data[sid][item.GenerationDate] = item; } @@ -37,13 +36,13 @@ public void Clear() data.Clear(); } - public bool Contains(HealthcheckData item) + public bool Contains(T item) { - var sid = item.DomainSid; + var sid = item.Domain.DomainSID; return data.ContainsKey(sid) && data[sid].ContainsKey(item.GenerationDate); } - public void CopyTo(HealthcheckData[] array, int arrayIndex) + public void CopyTo(T[] array, int arrayIndex) { foreach (var value in data.Values) { @@ -70,9 +69,9 @@ public bool IsReadOnly get { return false; } } - public bool Remove(HealthcheckData item) + public bool Remove(T item) { - var sid = item.DomainSid; + var sid = item.Domain.DomainSID; if (!data.ContainsKey(sid)) return false; var value = data[sid]; @@ -82,7 +81,7 @@ public bool Remove(HealthcheckData item) return true; } - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() { throw new NotImplementedException(); } @@ -92,6 +91,23 @@ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() return this.GetEnumerator(); } + public PingCastleReportCollection ToLatestReportCollection() + { + var output = new PingCastleReportCollection(); + foreach (var sid in data.Keys) + { + DateTime maxDate = DateTime.MinValue; + foreach (var date in data[sid].Keys) + { + if (maxDate < date) + maxDate = date; + } + output.Add(data[sid][maxDate]); + } + output.EnrichInformation(); + return output; + } + public IEnumerable> GetKeyPoints() { var output = new List>(); @@ -105,14 +121,14 @@ public IEnumerable> GetKeyPoints() return output; } - public HealthcheckData GetItem(KeyValuePair key) + public T GetItem(KeyValuePair key) { return data[key.Key][key.Value]; } - public HealthcheckDataCollection GetDataReportAtDate(DateTime dateToIssueReport) + public PingCastleReportCollection GetDataReportAtDate(DateTime dateToIssueReport) { - var output = new HealthcheckDataCollection(); + var output = new PingCastleReportCollection(); foreach (var sid in data.Keys) { DateTime min = DateTime.MinValue; @@ -124,6 +140,7 @@ public HealthcheckDataCollection GetDataReportAtDate(DateTime dateToIssueReport) if (min != DateTime.MinValue) output.Add(data[sid][min]); } + output.EnrichInformation(); return output; } diff --git a/Graph/Database/IDataStorage.cs b/Graph/Database/IDataStorage.cs index 2ec6915..76eed19 100644 --- a/Graph/Database/IDataStorage.cs +++ b/Graph/Database/IDataStorage.cs @@ -7,19 +7,39 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.ADWS; +using PingCastle.RPC; +using System.Security.Principal; -namespace PingCastle.Database +namespace PingCastle.Graph.Database { + public struct DataStorageDomainTrusts + { + public string NetbiosDomainName; + public string DnsDomainName; + public uint Flags; + public uint ParentIndex; + public uint TrustType; + public uint TrustAttributes; + public SecurityIdentifier DomainSid; + public Guid DomainGuid; + } + + public interface IDataStorage { int SearchItem(string name); - Dictionary RetrieveNodes(List nodes); - Dictionary GetDatabaseInformation(); - List SearchRelations(List SourceIds, List knownIds, bool FromMasterToSlave); + Node RetrieveNode(int id); + Dictionary RetrieveNodes(List nodes); + Dictionary GetDatabaseInformation(); + List SearchRelations(List SourceIds, List knownIds); - int InsertNode(string shortname, string objectclass, string name, string sid); + int InsertNode(string shortname, string objectclass, string name, string sid, ADItem adItem); void InsertRelation(string mappingMaster, MappingType typeMaster, string mappingSlave, MappingType typeSlave, RelationType relationType); - } + List GetKnownDomains(); + + bool IsSIDAlreadyInserted(string sid); + } } diff --git a/Graph/Database/LiveDataStorage.cs b/Graph/Database/LiveDataStorage.cs index 0408b5b..c3ce07e 100644 --- a/Graph/Database/LiveDataStorage.cs +++ b/Graph/Database/LiveDataStorage.cs @@ -5,26 +5,31 @@ // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. // using PingCastle.ADWS; +using PingCastle.RPC; using System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Text; -namespace PingCastle.Database +namespace PingCastle.Graph.Database { public class LiveDataStorage: IDataStorage { - Dictionary nodes; + public Dictionary nodes; int index; // first index = slave ; second index = master - Dictionary> relations; - Dictionary databaseInformation; + public Dictionary> relations; + public Dictionary databaseInformation; public List KnownCN = new List(); public List KnownSID = new List(); + public List KnownPGId = new List() { 513, 515 }; public List CNToInvestigate { get; private set; } public List SIDToInvestigate { get; private set; } + public List PGIdToInvestigate { get; private set; } + public List KnownDomains { get; private set; } + private string serverForSIDResolution; struct RelationOnHold { @@ -45,6 +50,8 @@ public LiveDataStorage() databaseInformation = new Dictionary(); SIDToInvestigate = new List(); CNToInvestigate = new List(); + PGIdToInvestigate = new List(); + KnownDomains = new List(); } public void Initialize(ADDomainInfo domainInfo) @@ -54,15 +61,14 @@ public void Initialize(ADDomainInfo domainInfo) databaseInformation["DomainName"] = domainInfo.DomainName; databaseInformation["DefaultNamingContext"] = domainInfo.DefaultNamingContext; databaseInformation["DomainSid"] = domainInfo.DomainSid.Value; + databaseInformation["DomainNetBIOS"] = domainInfo.NetBIOSName; + serverForSIDResolution = domainInfo.DnsHostName; } public List GetCNToInvestigate() { List output = new List(); - foreach (string s in CNToInvestigate) - { - output.Add(s); - } + output.AddRange(CNToInvestigate); CNToInvestigate.Clear(); return output; } @@ -70,21 +76,57 @@ public List GetCNToInvestigate() public List GetSIDToInvestigate() { List output = new List(); - foreach (string s in SIDToInvestigate) - { - output.Add(s); - } + output.AddRange(SIDToInvestigate); SIDToInvestigate.Clear(); return output; } - public int InsertNode(string shortname, string objectclass, string name, string sid) + public List GetPrimaryGroupIDToInvestigate() + { + List output = new List(); + output.AddRange(PGIdToInvestigate); + KnownPGId.AddRange(PGIdToInvestigate); + PGIdToInvestigate.Clear(); + return output; + } + + public int InsertNode(string shortname, string objectclass, string name, string sid, ADItem adItem) { - Node node = new Node(); + if (String.Equals(objectclass, "unknown", StringComparison.OrdinalIgnoreCase)) + { + if (name.Contains(",CN=ForeignSecurityPrincipals,DC=")) + { + objectclass = "foreignsecurityprincipal"; + sid = name.Substring(3, name.IndexOf(',') - 3); + } + } + // reentrance from previous if + if (String.Equals(objectclass, "foreignsecurityprincipal", StringComparison.OrdinalIgnoreCase)) + { + // avoid CREATOR OWNER (used for dynamic permissions) + if (String.Equals(sid, "S-1-3-0", StringComparison.OrdinalIgnoreCase)) + return -1; + if (String.Equals(sid, "S-1-5-18", StringComparison.OrdinalIgnoreCase)) + return -1; + string referencedDomain = null; + string ntaccount = NativeMethods.ConvertSIDToName(sid, serverForSIDResolution, out referencedDomain); + if (ntaccount == shortname) + { + if (String.IsNullOrEmpty(referencedDomain)) + ntaccount = shortname; + else + ntaccount = referencedDomain + "\\" + shortname; + } + shortname = ntaccount; + name = sid; + adItem = null; + } + Node node = new Node(); node.Id = index; node.Shortname = shortname; node.Type = objectclass; node.Dn = name; + node.ADItem = adItem; if (!String.IsNullOrEmpty(name)) { KnownCN.Add(name); @@ -98,10 +140,32 @@ public int InsertNode(string shortname, string objectclass, string name, string KnownSID.Add(sid); if (SIDToInvestigate.Contains(sid)) SIDToInvestigate.Remove(sid); + // handle primary group id + if (objectclass == "group") + { + if (sid.StartsWith("S-1-5-21-")) + { + var part = sid.Split('-'); + int PGId = int.Parse(part[part.Length - 1]); + if (!KnownPGId.Contains(PGId) && !PGIdToInvestigate.Contains(PGId)) + { + PGIdToInvestigate.Add(PGId); + } + } + } } return index++; } + public bool IsSIDAlreadyInserted(string sid) + { + if (KnownSID.Contains(sid)) + { + return true; + } + return false; + } + public void InsertRelation(string mappingMaster, MappingType typeMaster, string mappingSlave, MappingType typeSlave, RelationType relationType) { RelationOnHold relation = new RelationOnHold(); @@ -139,13 +203,13 @@ void AddDataToInvestigate(string mapping, MappingType type) } } - public void InsertRelationOnHold(string serverForSIDResolution) + public void InsertRelationOnHold() { foreach (RelationOnHold relation in relationsOnHold) { try { - InsertRelation(serverForSIDResolution, relation.mappingMaster, relation.typeMaster, relation.mappingSlave, relation.typeSlave, relation.relationType); + InsertRelationInternal(relation.mappingMaster, relation.typeMaster, relation.mappingSlave, relation.typeSlave, relation.relationType); } catch (Exception) { @@ -156,7 +220,7 @@ public void InsertRelationOnHold(string serverForSIDResolution) relationsOnHold.Clear(); } - private void InsertRelation(string serverForSIDResolution, string mappingMaster, MappingType typeMaster, string mappingSlave, MappingType typeSlave, RelationType relationType) + private void InsertRelationInternal(string mappingMaster, MappingType typeMaster, string mappingSlave, MappingType typeSlave, RelationType relationType) { int masteridx = GetIdx(mappingMaster, typeMaster); int slaveidx = GetIdx(mappingSlave, typeSlave); @@ -165,23 +229,22 @@ private void InsertRelation(string serverForSIDResolution, string mappingMaster, { if (typeMaster == MappingType.Sid) { - string ntaccount = NativeMethods.ConvertSIDToName(mappingMaster, serverForSIDResolution); - masteridx = InsertNode(mappingMaster, "unknown", ntaccount, mappingMaster); + masteridx = InsertNode(mappingMaster, "foreignsecurityprincipal", mappingMaster, mappingMaster, null); } else { - masteridx = InsertNode(mappingMaster, "unknown", mappingMaster, null); + masteridx = InsertNode(mappingMaster, "unknown", mappingMaster, null, null); } } if (slaveidx == -1) { - if (typeMaster == MappingType.Sid) + if (typeSlave == MappingType.Sid) { - slaveidx = InsertNode(mappingSlave, "unknown", mappingSlave, mappingSlave); + slaveidx = InsertNode(mappingSlave, "foreignsecurityprincipal", mappingSlave, mappingSlave, null); } else { - slaveidx = InsertNode(mappingSlave, "unknown", mappingSlave, null); + slaveidx = InsertNode(mappingSlave, "unknown", mappingSlave, null, null); } } @@ -255,12 +318,17 @@ private int GetIdx(string name, MappingType mappingType) return -1; } + public Node RetrieveNode(int id) + { + return nodes[id]; + } + public Dictionary RetrieveNodes(List nodesQueried) { Dictionary output = new Dictionary(); foreach (int node in nodesQueried) { - output.Add(node, nodes[node]); + output.Add(node, RetrieveNode(node)); } return output; } @@ -270,12 +338,8 @@ public Dictionary GetDatabaseInformation() return databaseInformation; } - public List SearchRelations(List SourceIds, List knownIds, bool FromMasterToSlave) + public List SearchRelations(List SourceIds, List knownIds) { - if (FromMasterToSlave) - { - throw new ApplicationException("reverse direction is not allowed in live mode (all Security Descriptors need to be analyzed)"); - } List output = new List(); foreach (int sourceId in SourceIds) { @@ -284,15 +348,20 @@ public List SearchRelations(List SourceIds, List knownIds, b var half = relations[sourceId]; foreach (int key in half.Keys) { - if (!knownIds.Contains(key)) - output.Add(half[key]); + if (!knownIds.Contains(key)) + { + var relation = half[key]; + output.Add(relation); + } } } } return output; } - - - } + public List GetKnownDomains() + { + return KnownDomains; + } + } } diff --git a/Graph/Database/MappingType.cs b/Graph/Database/MappingType.cs index bfb9b48..3e5678c 100644 --- a/Graph/Database/MappingType.cs +++ b/Graph/Database/MappingType.cs @@ -8,7 +8,7 @@ using System.Collections.Generic; using System.Text; -namespace PingCastle.Database +namespace PingCastle.Graph.Database { public enum MappingType { diff --git a/Graph/Database/Node.cs b/Graph/Database/Node.cs index f8e3e16..5d650d5 100644 --- a/Graph/Database/Node.cs +++ b/Graph/Database/Node.cs @@ -7,8 +7,9 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.ADWS; -namespace PingCastle.Database +namespace PingCastle.Graph.Database { public class Node { @@ -21,6 +22,8 @@ public class Node public int Distance { get; set; } + public ADItem ADItem { get; set; } + public string Name { get { diff --git a/Graph/Database/Relation.cs b/Graph/Database/Relation.cs index e9b0c3c..2deea81 100644 --- a/Graph/Database/Relation.cs +++ b/Graph/Database/Relation.cs @@ -8,7 +8,7 @@ using System.Collections.Generic; using System.Text; -namespace PingCastle.Database +namespace PingCastle.Graph.Database { public class Relation { diff --git a/Graph/Database/RelationType.cs b/Graph/Database/RelationType.cs index 8f7eb55..afd2dde 100644 --- a/Graph/Database/RelationType.cs +++ b/Graph/Database/RelationType.cs @@ -9,7 +9,7 @@ using System.ComponentModel; using System.Text; -namespace PingCastle.Database +namespace PingCastle.Graph.Database { // use [Description("")] attribute to change the record name in the database public enum RelationType diff --git a/Graph/Export/ExportDataFromActiveDirectoryLive.cs b/Graph/Export/ExportDataFromActiveDirectoryLive.cs index 7b46e99..da7bf47 100644 --- a/Graph/Export/ExportDataFromActiveDirectoryLive.cs +++ b/Graph/Export/ExportDataFromActiveDirectoryLive.cs @@ -5,11 +5,15 @@ // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. // using PingCastle.ADWS; -using PingCastle.Database; +using PingCastle.Graph.Database; +using PingCastle.Graph.Reporting; +using PingCastle.RPC; using System; using System.Collections.Generic; using System.Diagnostics; using System.Net; +using System.Runtime.InteropServices; +using System.Security.Principal; using System.Text; namespace PingCastle.Export @@ -19,28 +23,34 @@ public class ExportDataFromActiveDirectoryLive string Server; int Port; NetworkCredential Credential; - public LiveDataStorage Storage { get; private set; } - - string[] properties = new string[] { + string[] properties = new string[] { "distinguishedName", + "displayName", "name", "objectSid", + "objectClass", "nTSecurityDescriptor", "member", "adminCount", "gPLink", "gPCFileSysPath", + "lastLogonTimestamp", "scriptPath", "primaryGroupID", + "sAMAccountName", + "servicePrincipalName", "sIDHistory", + "userAccountControl", }; + public LiveDataStorage Storage { get; set; } + public ExportDataFromActiveDirectoryLive(string server, int port, NetworkCredential credential) { Server = server; Port = port; Credential = credential; - Storage = new LiveDataStorage(); + Storage = new LiveDataStorage(); } private void DisplayAdvancement(string data) @@ -50,26 +60,32 @@ private void DisplayAdvancement(string data) Trace.WriteLine(value); } - public void ExportData(List UsersToInvestigate) + public GraphObjectReference ExportData(List UsersToInvestigate) { ADDomainInfo domainInfo = null; RelationFactory relationFactory = null; - DisplayAdvancement("Getting domain informations"); + GraphObjectReference objectReference = null; + DisplayAdvancement("Getting domain information (" + Server + ")"); using (ADWebService adws = new ADWebService(Server, Port, Credential)) { domainInfo = GetDomainInformation(adws); - Storage.Initialize(domainInfo); + Storage.Initialize(domainInfo); Trace.WriteLine("Creating new relation factory"); - relationFactory = new RelationFactory(Storage, domainInfo, Credential); + relationFactory = new RelationFactory(Storage, domainInfo, Credential); DisplayAdvancement("Exporting objects from Active Directory"); - ExportReportData(adws, domainInfo, relationFactory, UsersToInvestigate); + objectReference = new GraphObjectReference(domainInfo); + ExportReportData(adws, domainInfo, relationFactory, Storage, objectReference, UsersToInvestigate); } DisplayAdvancement("Inserting relations between nodes in the database"); Trace.WriteLine("Inserting relations on hold"); - Storage.InsertRelationOnHold(domainInfo.DnsHostName); + Storage.InsertRelationOnHold(); + Trace.WriteLine("Add trusted domains"); + AddTrustedDomains(Storage); Trace.WriteLine("Done"); DisplayAdvancement("Export completed"); + DisplayAdvancement("Doing the analysis"); + return objectReference; } private ADDomainInfo GetDomainInformation(ADWebService adws) @@ -95,58 +111,62 @@ private ADDomainInfo GetDomainInformation(ADWebService adws) adws.Enumerate(domainInfo.DefaultNamingContext, "(&(objectClass=domain)(distinguishedName=" + domainInfo.DefaultNamingContext + "))", properties, callback); + // adding the domain Netbios name + string[] propertiesNetbios = new string[] { "nETBIOSName" }; + adws.Enumerate("CN=Partitions," + domainInfo.ConfigurationNamingContext, + "(&(objectCategory=crossRef)(systemFlags:1.2.840.113556.1.4.803:=3)(nETBIOSName=*)(nCName=" + domainInfo.DefaultNamingContext + "))", + propertiesNetbios, + (ADItem aditem) => + { + domainInfo.NetBIOSName = aditem.NetBIOSName; + } + , "OneLevel"); return domainInfo; } - private void ExportReportData(ADWebService adws, ADDomainInfo domainInfo, RelationFactory relationFactory, List UsersToInvestigate) + private void ExportReportData(ADWebService adws, ADDomainInfo domainInfo, RelationFactory relationFactory, LiveDataStorage storage, GraphObjectReference objectReference, List UsersToInvestigate) { - - List sids = new List {"S-1-5-32-548", - "S-1-5-32-544", - domainInfo.DomainSid.Value + "-512", - domainInfo.DomainSid.Value + "-519", - domainInfo.DomainSid.Value + "-518", - domainInfo.DomainSid.Value + "-500", - "S-1-5-32-551", - domainInfo.DomainSid.Value + "-517", - "S-1-5-32-569", - domainInfo.DomainSid.Value + "-516", - domainInfo.DomainSid.Value + "-498", - domainInfo.DomainSid.Value + "-520", - "S-1-5-32-557", - domainInfo.DomainSid.Value + "-502", - "S-1-5-32-556", - "S-1-5-32-554", - "S-1-5-32-550", - domainInfo.DomainSid.Value, - domainInfo.DomainSid.Value + "-521", - "S-1-5-32-549", - }; ADItem aditem = null; - foreach (string sid in sids) - { - aditem = Search(adws, domainInfo, sid); - if (aditem != null) - relationFactory.AnalyzeADObject(aditem); - else - Trace.WriteLine("Unable to find the user: " + sid); - } + foreach (var typology in objectReference.Objects.Keys) + { + var toDelete = new List(); + foreach (var obj in objectReference.Objects[typology]) + { + DisplayAdvancement("Working on " + obj.Description); + aditem = Search(adws, domainInfo, obj.Name); + if (aditem != null) + relationFactory.AnalyzeADObject(aditem); + else + { + Trace.WriteLine("Unable to find the user: " + obj.Description); + toDelete.Add(obj); + } + } + foreach(var obj in toDelete) + { + objectReference.Objects[typology].Remove(obj); + } + } foreach (string user in UsersToInvestigate) { + DisplayAdvancement("Working on " + user); aditem = Search(adws, domainInfo, user); if (aditem != null) + { + objectReference.Objects[Data.CompromiseGraphDataTypology.UserDefined].Add(new GraphSingleObject(user, user)); relationFactory.AnalyzeADObject(aditem); + } else Trace.WriteLine("Unable to find the user: " + user); } - AnalyzeMissingObjets(adws, domainInfo, relationFactory); + AnalyzeMissingObjets(adws, domainInfo, relationFactory, storage); relationFactory.InsertFiles(); - AnalyzeMissingObjets(adws, domainInfo, relationFactory); + AnalyzeMissingObjets(adws, domainInfo, relationFactory, storage); } - int AnalyzeMissingObjets(ADWebService adws, ADDomainInfo domainInfo, RelationFactory relationFactory) + int AnalyzeMissingObjets(ADWebService adws, ADDomainInfo domainInfo, RelationFactory relationFactory, LiveDataStorage Storage) { int num = 0; while (true) @@ -158,15 +178,21 @@ int AnalyzeMissingObjets(ADWebService adws, ADDomainInfo domainInfo, RelationFac ExportCNData(adws, domainInfo, relationFactory, cns); } List sids = Storage.GetSIDToInvestigate(); - if (cns.Count == 0 && sids.Count == 0) - { - return num; - } if (sids.Count > 0) { num += sids.Count; ExportSIDData(adws, domainInfo, relationFactory, sids); } + List primaryGroupId = Storage.GetPrimaryGroupIDToInvestigate(); + if (primaryGroupId.Count > 0) + { + num += primaryGroupId.Count; + ExportPrimaryGroupData(adws, domainInfo, relationFactory, primaryGroupId); + } + if (cns.Count == 0 && sids.Count == 0 && primaryGroupId.Count == 0) + { + return num; + } } } @@ -202,6 +228,22 @@ private void ExportSIDData(ADWebService adws, ADDomainInfo domainInfo, RelationF } } + private void ExportPrimaryGroupData(ADWebService adws, ADDomainInfo domainInfo, RelationFactory relationFactory, List primaryGroupIDs) + { + WorkOnReturnedObjectByADWS callback = + (ADItem aditem) => + { + relationFactory.AnalyzeADObject(aditem); + }; + + foreach (int id in primaryGroupIDs) + { + adws.Enumerate(domainInfo.DefaultNamingContext, + "(primaryGroupID=" + id + ")", + properties, callback); + } + } + private ADItem Search(ADWebService adws, ADDomainInfo domainInfo, string userName) { ADItem output = null; @@ -216,13 +258,25 @@ private ADItem Search(ADWebService adws, ADDomainInfo domainInfo, string userNam adws.Enumerate(domainInfo.DefaultNamingContext, "(objectSid=" + ADConnection.EncodeSidToString(userName) + ")", properties, callback); + if (output != null) + return output; } - - adws.Enumerate(domainInfo.DefaultNamingContext, - "(&(objectCategory=person)(objectClass=user)(sAMAccountName=" + ADConnection.EscapeLDAP(userName) + "))", - properties, callback); - if (output != null) - return output; + if (userName.StartsWith("CN=") && userName.EndsWith(domainInfo.DefaultNamingContext)) + { + adws.Enumerate(domainInfo.DefaultNamingContext, + "(distinguishedName=" + ADConnection.EscapeLDAP(userName) + ")", + properties, callback); + if (output != null) + return output; + } + if (userName.Length <= 20) + { + adws.Enumerate(domainInfo.DefaultNamingContext, + "(&(objectCategory=person)(objectClass=user)(sAMAccountName=" + ADConnection.EscapeLDAP(userName) + "))", + properties, callback); + if (output != null) + return output; + } adws.Enumerate(domainInfo.DefaultNamingContext, "(cn=" + ADConnection.EscapeLDAP(userName) + ")", properties, callback); @@ -235,5 +289,75 @@ private ADItem Search(ADWebService adws, ADDomainInfo domainInfo, string userNam return output; return output; } + + + private List GetAllDomainTrusts(string server) + { + var output = new List(); + IntPtr ptr = IntPtr.Zero; + uint DomainCount = 0; + uint error = NativeMethods.DsEnumerateDomainTrusts(server, (uint)NativeMethods.DS_DOMAIN_TRUST_TYPE.ALL, out ptr, out DomainCount); + if (error == 0) + { + for (int i = 0; i < DomainCount; i++) + { + IntPtr p = new IntPtr((ptr.ToInt64() + i * Marshal.SizeOf(typeof(NativeMethods.DS_DOMAIN_TRUSTS)))); + var domain = (NativeMethods.DS_DOMAIN_TRUSTS)Marshal.PtrToStructure(p, typeof(NativeMethods.DS_DOMAIN_TRUSTS)); + + output.Add(new DataStorageDomainTrusts() + { + NetbiosDomainName = domain.NetbiosDomainName, + DnsDomainName = domain.DnsDomainName, + Flags = domain.Flags, + ParentIndex = domain.ParentIndex, + TrustType = domain.TrustType, + TrustAttributes = domain.TrustAttributes, + DomainSid = domain.DomainSid != IntPtr.Zero ? new SecurityIdentifier(domain.DomainSid) : null, + DomainGuid = domain.DomainGuid, + }); + } + NativeMethods.NetApiBufferFree(ptr); + } + return output; + } + + private void AddTrustedDomains(LiveDataStorage storage) + { + storage.KnownDomains.Clear(); + List domains; + List KnownSID = new List(); + + + domains = GetAllDomainTrusts(Server); + storage.KnownDomains.AddRange(domains); + KnownSID.AddRange( domains.ConvertAll(x => x.DomainSid)); + + var domainLocator = new DomainLocator(Server); + foreach (var node in storage.nodes.Values) + { + if (!String.IsNullOrEmpty(node.Sid) && node.Sid.StartsWith("S-1-5-21-") && node.Shortname.Contains("\\")) + { + var sid = new SecurityIdentifier(node.Sid); + var domainSid = sid.AccountDomainSid; + if (!KnownSID.Contains(domainSid)) + { + string domainName; + string forestName; + string NetbiosName = node.Shortname.Split('\\')[0]; + if (domainLocator.LocateDomainFromNetbios(NetbiosName, out domainName, out forestName)) + { + KnownSID.Add(domainSid); + storage.KnownDomains.Add(new DataStorageDomainTrusts() + { + DnsDomainName = domainName, + DomainSid = domainSid, + NetbiosDomainName = NetbiosName, + } + ); + } + } + } + } + } } } diff --git a/Graph/Export/RelationFactory.cs b/Graph/Export/RelationFactory.cs index a765c0e..c16711c 100644 --- a/Graph/Export/RelationFactory.cs +++ b/Graph/Export/RelationFactory.cs @@ -5,7 +5,7 @@ // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. // using PingCastle.ADWS; -using PingCastle.Database; +using PingCastle.Graph.Database; using PingCastle.misc; using System; using System.Collections.Generic; @@ -40,26 +40,35 @@ public RelationFactory(IDataStorage storage, ADDomainInfo domainInfo, NetworkCre Credential = credential; } - static KeyValuePair[] GuidsControlExtendedRights = new KeyValuePair[] { + public static KeyValuePair[] GuidsControlExtendedRights = new KeyValuePair[] { new KeyValuePair(new Guid("00299570-246d-11d0-a768-00aa006e0529"), RelationType.EXT_RIGHT_FORCE_CHANGE_PWD), new KeyValuePair(new Guid("1131f6ad-9c07-11d1-f79f-00c04fc2dcd2"), RelationType.EXT_RIGHT_REPLICATION_GET_CHANGES_ALL), }; - static KeyValuePair[] GuidsControlValidatedWrites = new KeyValuePair[] { + public static KeyValuePair[] GuidsControlValidatedWrites = new KeyValuePair[] { new KeyValuePair(new Guid("bc0ac240-79a9-11d0-9020-00c04fc2d4cf"),RelationType.WRITE_PROPSET_MEMBERSHIP), }; - static KeyValuePair[] GuidsControlProperties = new KeyValuePair[] { + public static KeyValuePair[] GuidsControlProperties = new KeyValuePair[] { new KeyValuePair(new Guid("bf9679c0-0de6-11d0-a285-00aa003049e2"),RelationType.WRITE_PROP_MEMBER), new KeyValuePair(new Guid("f30e3bbe-9ff0-11d1-b603-0000f80367c1"),RelationType.WRITE_PROP_GPLINK), new KeyValuePair(new Guid("f30e3bc1-9ff0-11d0-b603-0000f80367c1"),RelationType.WRITE_PROP_GPC_FILE_SYS_PATH), }; - static KeyValuePair[] GuidsControlPropertiesSets = new KeyValuePair[] { + public static KeyValuePair[] GuidsControlPropertiesSets = new KeyValuePair[] { new KeyValuePair(new Guid("bf9679c0-0de6-11d0-a285-00aa003049e2"),RelationType.VAL_WRITE_SELF_MEMBERSHIP), }; public void AnalyzeADObject(ADItem aditem) { + // avoid reentry which can be caused by primary group id checks + if (aditem.ObjectSid != null) + { + if (Storage.IsSIDAlreadyInserted(aditem.ObjectSid.Value)) + { + Trace.WriteLine("Item " + aditem.DistinguishedName + " has already been analyzed"); + return; + } + } Trace.WriteLine("Working on " + aditem.DistinguishedName); InsertNode(aditem); if (String.Equals(aditem.Class, "foreignsecurityprincipal", StringComparison.OrdinalIgnoreCase)) @@ -72,7 +81,11 @@ public void AnalyzeADObject(ADItem aditem) private void InsertNode(ADItem aditem) { - string shortname = aditem.Name; + string shortname = aditem.DisplayName; + if (String.IsNullOrEmpty(shortname)) + { + shortname = aditem.Name; + } //if (aditem.Class.Equals("foreignSecurityPrincipal", StringComparison.InvariantCultureIgnoreCase) && aditem.ObjectSid != null) //{ // shortname = NativeMethods.ConvertSIDToName(aditem.ObjectSid.Value, null); @@ -86,12 +99,12 @@ private void InsertNode(ADItem aditem) else shortname = m.Groups[1].Value; } - Storage.InsertNode(shortname, aditem.Class, aditem.DistinguishedName, (aditem.ObjectSid != null ? aditem.ObjectSid.Value : null)); + Storage.InsertNode(shortname, aditem.Class.ToLowerInvariant(), aditem.DistinguishedName, (aditem.ObjectSid != null ? aditem.ObjectSid.Value : null), aditem); } public void InsertFileNode(string file) { - Storage.InsertNode(file, "file", file, null); + Storage.InsertNode(file, "file", file, null, null); } private void AddFileRelation(ADItem aditem) diff --git a/Graph/Reporting/GraphObjectReference.cs b/Graph/Reporting/GraphObjectReference.cs new file mode 100644 index 0000000..16ad447 --- /dev/null +++ b/Graph/Reporting/GraphObjectReference.cs @@ -0,0 +1,68 @@ +using PingCastle.ADWS; +using PingCastle.Data; +using System; +using System.Collections.Generic; +using System.Text; + +namespace PingCastle.Graph.Reporting +{ + public class GraphSingleObject + { + public GraphSingleObject(string name, string description, CompromiseGraphDataObjectRisk risk = CompromiseGraphDataObjectRisk.Other) + { + Name = name; + Description = description; + Risk = risk; + } + public string Name { get; set; } + public string Description { get; set; } + public CompromiseGraphDataObjectRisk Risk { get; set; } + } + + public class GraphObjectReference + { + public Dictionary> Objects {get;set;} + + public GraphObjectReference(ADDomainInfo data) + { + Objects = new Dictionary>() + { + {CompromiseGraphDataTypology.PrivilegedAccount, new List(){ + new GraphSingleObject("S-1-5-32-544","Administrators", CompromiseGraphDataObjectRisk.Critical), + new GraphSingleObject("S-1-5-32-548","Account Operator", CompromiseGraphDataObjectRisk.High), + new GraphSingleObject("S-1-5-32-549","Server Operators", CompromiseGraphDataObjectRisk.High), + new GraphSingleObject("S-1-5-32-550","Print Operators", CompromiseGraphDataObjectRisk.Medium), + new GraphSingleObject("S-1-5-32-551","Backup Operators", CompromiseGraphDataObjectRisk.High), + new GraphSingleObject("S-1-5-32-556","Network Operators", CompromiseGraphDataObjectRisk.Medium), + new GraphSingleObject("S-1-5-32-557","Incoming Forest Trust Builders", CompromiseGraphDataObjectRisk.Medium), + new GraphSingleObject("S-1-5-32-569","Certificate Operators", CompromiseGraphDataObjectRisk.Medium), + new GraphSingleObject(data.DomainSid.Value + "-500","Administrator", CompromiseGraphDataObjectRisk.Critical), + new GraphSingleObject(data.DomainSid.Value + "-512","Domain Administrators", CompromiseGraphDataObjectRisk.Critical), + new GraphSingleObject(data.DomainSid.Value + "-517","Certificate Publishers"), + new GraphSingleObject(data.DomainSid.Value + "-518","Schema Administrators", CompromiseGraphDataObjectRisk.Critical), + new GraphSingleObject(data.DomainSid.Value + "-519","Enterprise Administrators", CompromiseGraphDataObjectRisk.Critical), + }}, + {CompromiseGraphDataTypology.Infrastructure, new List(){ + new GraphSingleObject(data.DomainSid.Value,"Domain Root", CompromiseGraphDataObjectRisk.Medium), + new GraphSingleObject(data.DomainSid.Value + "-498","Enterprise Read Only Domain Controllers"), + new GraphSingleObject(data.DomainSid.Value + "-502","Krbtgt account", CompromiseGraphDataObjectRisk.Medium), + new GraphSingleObject(data.DomainSid.Value + "-516","Domain Controllers", CompromiseGraphDataObjectRisk.Critical), + new GraphSingleObject(data.DomainSid.Value + "-520","Group Policy Creator Owners", CompromiseGraphDataObjectRisk.Medium), + new GraphSingleObject(data.DomainSid.Value + "-521","Read Only Domain Controllers", CompromiseGraphDataObjectRisk.Medium), + new GraphSingleObject("CN=Builtin," + data.DefaultNamingContext,"Builtin OU", CompromiseGraphDataObjectRisk.Medium), + }}, + {CompromiseGraphDataTypology.UserDefined, new List(){ + }}, + }; + foreach (var typology in Objects.Keys) + { + Objects[typology].Sort((GraphSingleObject a, GraphSingleObject b) + => + { + return String.Compare(a.Description, b.Description); + }); + } + } + } + +} diff --git a/Graph/Reporting/ReportGenerator.cs b/Graph/Reporting/ReportGenerator.cs index 17be8a3..4cd5857 100644 --- a/Graph/Reporting/ReportGenerator.cs +++ b/Graph/Reporting/ReportGenerator.cs @@ -5,36 +5,41 @@ // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. // using PingCastle.Data; -using PingCastle.Database; -using PingCastle.Healthcheck; -using PingCastle.template; +using PingCastle.Graph.Database; +using PingCastle.Graph; using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Reflection; -using System.Text; +using System.Security.Principal; +using PingCastle.ADWS; +using PingCastle.Graph.Rules; +using PingCastle.Rules; +using System.Net; +using PingCastle.Export; +using PingCastle.Graph.Reporting; namespace PingCastle.Reporting { - public class ReportGenerator + public class ReportGenerator : IPingCastleAnalyzer { - private int MaxDepth; - private int MaxNodes; - private IDataStorage storage; + public static int MaxDepth { get; set; } + public static int MaxNodes { get; set; } - public ReportGenerator(IDataStorage storage, int MaxDepth, int MaxNodes) + static ReportGenerator() { - this.MaxDepth = MaxDepth; - this.MaxNodes = MaxNodes; - this.storage = storage; + MaxDepth = 30; + MaxNodes = 1000; } - public static bool JasonOnly { get; set; } - + private IDataStorage storage; + private List stopNodes = new List(); - public CompromiseGraphData GenerateReport(List AdditionalNames) + public CompromiseGraphData PerformAnalyze(PingCastleAnalyzerParameters parameters) { + ExportDataFromActiveDirectoryLive export = new ExportDataFromActiveDirectoryLive(parameters.Server, parameters.Port, parameters.Credential); + var ObjectReference = export.ExportData(parameters.AdditionalNamesForDelegationAnalysis); + storage = export.Storage; CompromiseGraphData data = new CompromiseGraphData(); data.GenerationDate = DateTime.Now; Version version = Assembly.GetExecutingAssembly().GetName().Version; @@ -46,36 +51,280 @@ public CompromiseGraphData GenerateReport(List AdditionalNames) Dictionary databaseProperties = storage.GetDatabaseInformation(); data.DomainSid = databaseProperties["DomainSid"]; data.DomainFQDN = databaseProperties["DomainName"]; + data.DomainNetBIOS = databaseProperties["DomainNetBIOS"]; data.Data = new List(); + string domainContext = "DC=" + string.Join(",DC=", data.DomainFQDN.Split('.')); - ProduceReportFile(data, "Account Operator", "S-1-5-32-548"); - ProduceReportFile(data, "Administrators", "S-1-5-32-544"); - ProduceReportFile(data, "Domain Administrators", data.DomainSid + "-512"); - ProduceReportFile(data, "Enterprise Administrators", data.DomainSid + "-519"); - ProduceReportFile(data, "Schema Administrators", data.DomainSid + "-518"); - ProduceReportFile(data, "Administrator", data.DomainSid + "-500"); - ProduceReportFile(data, "Backup Operators", "S-1-5-32-551"); - ProduceReportFile(data, "Certificate Publishers", data.DomainSid + "-517"); - ProduceReportFile(data, "Certificate Operators", "S-1-5-32-569"); - ProduceReportFile(data, "Domain Controllers", data.DomainSid + "-516"); - ProduceReportFile(data, "Enterprise Read Only Domain Controllers", data.DomainSid + "-498"); - ProduceReportFile(data, "Group Policy Creator Owners", data.DomainSid + "-520"); - ProduceReportFile(data, "Incoming Forest Trust Builders", "S-1-5-32-557"); - ProduceReportFile(data, "Krbtgt account", data.DomainSid + "-502"); - ProduceReportFile(data, "Network Operators", "S-1-5-32-556"); - ProduceReportFile(data, "Pre-Windows 2000 Compatible Access", "S-1-5-32-554"); - ProduceReportFile(data, "Print Operators", "S-1-5-32-550"); - ProduceReportFile(data, "Domain Root", data.DomainSid); - ProduceReportFile(data, "Read Only Domain Controllers", data.DomainSid + "-521"); - ProduceReportFile(data, "Server Operators", "S-1-5-32-549"); - foreach (string name in AdditionalNames) - { - ProduceReportFile(data, name, name, true); - } + PrepareStopNodes(ObjectReference); + + PrepareDetailledData(data, ObjectReference); + PrepareDependancyGlobalData(data); + PrepareAnomalyAnalysisData(data); + PrepareRiskData(data); + //PrepareObjectiveData(data); return data; } - private void ProduceReportFile(CompromiseGraphData data, string description, string name, bool onDemand = false) + void PrepareDetailledData(CompromiseGraphData data, GraphObjectReference ObjectReference) + { + foreach (var typology in ObjectReference.Objects.Keys) + { + foreach (var obj in ObjectReference.Objects[typology]) + { + ProduceReportFile(data, typology, obj.Risk, obj.Description, obj.Name); + } + } + data.Data.Sort( + (SingleCompromiseGraphData a, SingleCompromiseGraphData b) + => + { + return string.Compare(a.Description, b.Description); + }); + } + + private void PrepareDependancyGlobalData(CompromiseGraphData data) + { + var reference = new Dictionary(); + foreach (var sg in data.Data) + { + foreach (SingleCompromiseGraphDependancyData d in sg.Dependancies) + { + // beware: we are using exising SingleCompromiseGraphDependancyData with data a key + // do not modify the object !!!! + if (!reference.ContainsKey(d.Sid)) + { + reference[d.Sid] = new CompromiseGraphDependancyData(); + reference[d.Sid].Details = new List(); + reference[d.Sid].FQDN = d.FQDN; + reference[d.Sid].Netbios = d.Netbios; + reference[d.Sid].Sid = d.Sid; + } + var gdData = reference[d.Sid]; + CompromiseGraphDependancyDetailData detail = null; + foreach (var a in gdData.Details) + { + if (a.Typology == sg.Typology) + detail = a; + } + if (detail == null) + { + detail = new CompromiseGraphDependancyDetailData() + { + Typology = sg.Typology, + Items = new List(), + }; + reference[d.Sid].Details.Add(detail); + } + detail.NumberOfGroupImpacted++; + foreach (var item in d.Items) + { + if (!detail.Items.Contains(item.Sid)) + { + detail.Items.Add(item.Sid); + if (item.Name.Contains("\\")) + detail.NumberOfResolvedItems++; + else + detail.NumberOfUnresolvedItems++; + } + } + } + } + data.Dependancies = new List(reference.Values); + data.Dependancies.Sort((CompromiseGraphDependancyData a, CompromiseGraphDependancyData b) + => + { + return string.Compare(a.Netbios, b.Netbios); + }); + } + + private void PrepareAnomalyAnalysisData(CompromiseGraphData data) + { + var reference = new Dictionary(); + foreach (var sg in data.Data) + { + CompromiseGraphAnomalyAnalysisData analysis = null; + if (!reference.ContainsKey(sg.ObjectRisk)) + { + analysis = new CompromiseGraphAnomalyAnalysisData() + { + ObjectRisk = sg.ObjectRisk, + }; + reference[sg.ObjectRisk] = analysis; + } + else + { + analysis = reference[sg.ObjectRisk]; + } + analysis.NumberOfObjectsScreened++; + if (sg.CriticalObjectFound) + { + analysis.CriticalObjectFound = true; + } + if (sg.IndirectMembers.Count > 0) + { + if (analysis.MaximumIndirectNumber < sg.IndirectMembers.Count) + analysis.MaximumIndirectNumber = sg.IndirectMembers.Count; + analysis.NumberOfObjectsWithIndirect++; + if (sg.DirectUserMembers.Count > 0) + { + int ratio = 100 * sg.IndirectMembers.Count / sg.DirectUserMembers.Count; + if (ratio > analysis.MaximumDirectIndirectRatio) + analysis.MaximumDirectIndirectRatio = ratio; + } + } + + + } + data.AnomalyAnalysis = new List(); + data.AnomalyAnalysis.AddRange(reference.Values); + data.AnomalyAnalysis.Sort( + (CompromiseGraphAnomalyAnalysisData a, CompromiseGraphAnomalyAnalysisData b) + => + { + return ((int)a.ObjectRisk).CompareTo((int)b.ObjectRisk); + }); + } + + private void PrepareRiskData(CompromiseGraphData data) + { + data.RiskRules = new List(); + var rules = new RuleSet(); + foreach (var rule in rules.ComputeRiskRules(data)) + { + var risk = new CompromiseGraphRiskRule(); + if (rule.Points == 0) + risk.Achieved = true; + risk.Points = rule.RuleComputation[0].Score; + risk.Category = rule.Category; + risk.Objective = rule.Objective; + risk.RiskId = rule.RiskId; + risk.Rationale = rule.Rationale + String.Empty; + risk.Details = rule.Details; + risk.ImpactedAssets = new List(); + var graphRule = rule as CompromiseGraphRule; + if (graphRule != null) + { + foreach (var assetName in graphRule.ImpactedGraph.Keys) + { + var asset = graphRule.ImpactedGraph[assetName]; + var impactedAsset = new CompromiseGraphRiskRuleDetail(); + impactedAsset.AssetName = assetName; + impactedAsset.Details = asset.Details; + impactedAsset.Rationale = asset.Rationale; + risk.ImpactedAssets.Add(impactedAsset); + } + } + data.RiskRules.Add(risk); + } + data.RiskRules.Sort((CompromiseGraphRiskRule a, CompromiseGraphRiskRule b) + => + { + int compare = ((int)a.Objective).CompareTo((int)b.Objective); + if (compare == 0) + { + compare = -a.Points.CompareTo(b.Points); + } + if (compare == 0) + { + compare = a.Rationale.CompareTo(b.Rationale); + } + return compare; + } + ); + } + /* + private void PrepareObjectiveData(CompromiseGraphData data) + { + data.Objectives = new List(); + data.Objectives.Add(new CompromiseGraphObjective() + { + Category = RiskRuleCategory.Trusts, + Objective = "No more than 1 domain can take control of an admin or critical object", + Score = 100, + RulesMatched = new List() { "A-TEST" }, + }); + data.Objectives.Add(new CompromiseGraphObjective() + { + Category = RiskRuleCategory.Trusts, + Objective = "No domain can take control of an admin or critical object", + Score = 100, + }); + data.Objectives.Add(new CompromiseGraphObjective() + { + Category = RiskRuleCategory.Trusts, + Objective = "No domain can take control of a user defined object", + Score = 25, + }); + data.Objectives.Add(new CompromiseGraphObjective() + { + Category = RiskRuleCategory.Trusts, + Objective = "At the exception of a domain declared as an admin domain, no child domain of a forest should have permission on other domains", + Score = 25, + }); + bool criticalfound = false; + foreach (var anomaly in data.AnomalyAnalysis) + { + string risk = null; + int basescore; + if (anomaly.CriticalObjectFound) + criticalfound = true; + switch (anomaly.ObjectRisk) + { + case CompromiseGraphDataObjectRisk.Critical: + risk = "critical"; + basescore = 100; + break; + case CompromiseGraphDataObjectRisk.High: + risk = "high"; + basescore = 80; + break; + case CompromiseGraphDataObjectRisk.Medium: + risk = "medium"; + basescore = 60; + break; + default: + continue; + + } + data.Objectives.Add(new CompromiseGraphObjective() + { + Category = RiskRuleCategory.Anomalies, + Objective = "No " + risk + " priority object should be available to more than 100 indirect number", + Score = basescore, + IsAchieved = anomaly.MaximumIndirectNumber < 100, + }); + data.Objectives.Add(new CompromiseGraphObjective() + { + Category = RiskRuleCategory.Anomalies, + Objective = "No " + risk + " object should be available to more than 50 indirect number", + Score = basescore - 10, + IsAchieved = anomaly.MaximumIndirectNumber < 50, + }); + data.Objectives.Add(new CompromiseGraphObjective() + { + Category = RiskRuleCategory.Anomalies, + Objective = "No " + risk + " object should be available to more than 10 indirect number", + Score = basescore - 20, + IsAchieved = anomaly.MaximumIndirectNumber < 10, + }); + data.Objectives.Add(new CompromiseGraphObjective() + { + Category = RiskRuleCategory.Anomalies, + Objective = "No " + risk + " object should be available to indirect users at all", + Score = basescore - 30, + IsAchieved = anomaly.MaximumIndirectNumber == 0, + }); + } + data.Objectives.Add(new CompromiseGraphObjective() + { + Category = RiskRuleCategory.Anomalies, + Objective = "No object should allow Everyone, Authenticated Users, Domain Users or Domain Computers to take control of itself", + Score = 100, + IsAchieved = criticalfound, + }); + }*/ + + private void ProduceReportFile(CompromiseGraphData data, CompromiseGraphDataTypology typology, CompromiseGraphDataObjectRisk risk, string description, string name) { try { @@ -85,19 +334,31 @@ private void ProduceReportFile(CompromiseGraphData data, string description, str int rootNodeId = storage.SearchItem(name); if (rootNodeId < 0) { + // do not display error message for schema admin and enterprise admins which are missing on child domains + if (typology == CompromiseGraphDataTypology.PrivilegedAccount && (name.EndsWith("-519") || name.EndsWith("-518") || name.EndsWith("-498") || name.Equals("S-1-5-32-557"))) + return; Trace.WriteLine("Id not found for name=" + name); Console.WriteLine("The report " + description + " starting from " + name + " couldn't be built because the object wasn't found"); return; } List nodesid = new List(); Dictionary> links = RetrieveLinks(rootNodeId, nodesid); - Dictionary nodes = storage.RetrieveNodes(nodesid); - SimplifyGraph(nodes, links); - ComputeDistance(rootNodeId, links, nodes); - var singleCompromiseData = BuildSingleCompromiseGraphData(rootNodeId, nodes, links); + Dictionary chartNodes = storage.RetrieveNodes(nodesid); + List directUsers = RetrieveDirectUserNodes(rootNodeId, new string[] { "user", "msDS-GroupManagedServiceAccount", "msDS-ManagedServiceAccount" }); + List directComputers = RetrieveDirectUserNodes(rootNodeId, new string[] { "computer" }); + SimplifyGraph(chartNodes, links); + ComputeDistance(rootNodeId, links, chartNodes); + var singleCompromiseData = BuildSingleCompromiseGraphData(rootNodeId, chartNodes, links, directUsers); singleCompromiseData.Name = name; singleCompromiseData.Description = description; - singleCompromiseData.OnDemandAnalysis = onDemand; + singleCompromiseData.Typology = typology; + singleCompromiseData.ObjectRisk = risk; + + BuildUserMembers(singleCompromiseData, directUsers); + BuildComputerMembers(singleCompromiseData, directComputers); + BuildIndirectMembers(singleCompromiseData); + BuildDependancies(data, singleCompromiseData, chartNodes); + BuildDeletedObjects(data, singleCompromiseData, chartNodes); data.Data.Add(singleCompromiseData); } catch (Exception ex) @@ -114,6 +375,262 @@ private void ProduceReportFile(CompromiseGraphData data, string description, str } } + private void BuildDependancies(CompromiseGraphData refData, SingleCompromiseGraphData singleCompromiseData, Dictionary chartNodes) + { + var reference = new Dictionary(); + var domains = storage.GetKnownDomains(); + foreach (var node in chartNodes.Values) + { + if (String.Equals(node.Type, "foreignsecurityprincipal", StringComparison.InvariantCultureIgnoreCase)) + { + // ignore deleted accounts + if (node.Sid.StartsWith(refData.DomainSid + "-")) + continue; + SingleCompromiseGraphDependancyData data; + var sid = new SecurityIdentifier(node.Sid); + var domainSid = sid.AccountDomainSid; + if (domainSid == null) + continue; + if (!reference.ContainsKey(domainSid)) + { + data = new SingleCompromiseGraphDependancyData(); + reference[domainSid] = data; + data.Sid = domainSid.Value; + foreach (var domain in domains) + { + if (String.Equals(domain.DomainSid.Value, data.Sid, StringComparison.InvariantCultureIgnoreCase)) + { + data.FQDN = domain.DnsDomainName; + data.Netbios = domain.NetbiosDomainName; + break; + } + } + data.Items = new List(); + } + else + { + data = reference[domainSid]; + } + if (node.Shortname.Contains("\\")) + { + if (String.IsNullOrEmpty(data.Netbios)) + { + data.Netbios = node.Shortname.Split('\\')[0]; + } + data.NumberOfResolvedItems++; + } + else + { + data.NumberOfUnresolvedItems++; + } + data.Items.Add(new SingleCompromiseGraphDependancyMemberData() + { + Name = node.Shortname, + Sid = node.Sid, + } + ); + } + } + singleCompromiseData.Dependancies = new List(reference.Values); + singleCompromiseData.Dependancies.Sort( + (SingleCompromiseGraphDependancyData a, SingleCompromiseGraphDependancyData b) + => + { + return String.Compare(a.Netbios, b.Netbios); + } + ); + } + + + private void BuildDeletedObjects(CompromiseGraphData data, SingleCompromiseGraphData singleCompromiseData, Dictionary chartNodes) + { + singleCompromiseData.DeletedObjects = new List(); + foreach (var node in chartNodes.Values) + { + if (String.Equals(node.Type, "foreignsecurityprincipal", StringComparison.InvariantCultureIgnoreCase)) + { + // ignore everything but deleted accounts + if (node.Sid.StartsWith(data.DomainSid + "-")) + { + singleCompromiseData.DeletedObjects.Add(new SingleCompromiseGraphDeletedData() + { + Sid = node.Sid, + } + ); + } + } + } + } + + private void BuildUserMembers(SingleCompromiseGraphData singleCompromiseData, List directNodes) + { + singleCompromiseData.DirectUserMembers = new List(); + foreach (var id in directNodes) + { + var node = storage.RetrieveNode(id); + var user = BuildMembersUser(node.ADItem); + singleCompromiseData.DirectUserMembers.Add(user); + } + } + + private void BuildComputerMembers(SingleCompromiseGraphData singleCompromiseData, List directNodes) + { + singleCompromiseData.DirectComputerMembers = new List(); + foreach (var id in directNodes) + { + var node = storage.RetrieveNode(id); + var user = BuildMembersComputer(node.ADItem); + singleCompromiseData.DirectComputerMembers.Add(user); + } + } + + private void BuildIndirectMembers(SingleCompromiseGraphData singleCompromiseData) + { + singleCompromiseData.IndirectMembers = new List(); + var map = new Dictionary(); + foreach (var link in singleCompromiseData.Links) + { + map[link.Source] = link.Target; + } + var reference = new Dictionary(); + foreach (var node in singleCompromiseData.Nodes) + { + reference[node.Id] = node; + } + foreach (var node in singleCompromiseData.Nodes) + { + if (node.Type == "user" && node.Suspicious) + { + var user = BuildIndirectMemberUser(singleCompromiseData, node, reference, map); + singleCompromiseData.IndirectMembers.Add(user); + } + } + } + + private SingleCompromiseGraphIndirectMemberData BuildIndirectMemberUser(SingleCompromiseGraphData singleCompromiseData, SingleCompromiseGraphNodeData node, Dictionary reference, Dictionary map) + { + var member = new SingleCompromiseGraphIndirectMemberData(); + member.Name = node.ShortName; + member.Distance = node.Distance; + if (node.ADItem.ObjectSid != null) + member.Sid = node.ADItem.ObjectSid.Value; + int id = node.Id; + var currentNode = node; + var path = new List(); + while (id >= 0 && !(currentNode.Type == "user" && !currentNode.Suspicious)) + { + path.Add(currentNode.ShortName); + if (map.ContainsKey(id)) + { + id = map[id]; + currentNode = reference[id]; + } + else + id = -1; + } + if (id >= 0) + { + path.Add(currentNode.ShortName); + member.AuthorizedObject = currentNode.ShortName; + } + if (path.Count > 4) + { + member.Path = path[0] + "->" + path[1] + "->...->" + path[path.Count - 2] + "->" + path[path.Count - 1]; + } + else + { + member.Path = string.Join("->", path.ToArray()); + } + return member; + } + + private SingleCompromiseGraphUserMemberData BuildMembersUser(ADItem x) + { + var member = new SingleCompromiseGraphUserMemberData(); + member.Name = x.SAMAccountName; + member.DistinguishedName = x.DistinguishedName; + member.PwdLastSet = x.PwdLastSet; + member.LastLogonTimestamp = x.LastLogonTimestamp; + if ((x.UserAccountControl & 0x00000002) != 0) + { + } + else + { + member.IsEnabled = true; + // last login since 6 months + if (x.LastLogonTimestamp.AddDays(6 * 31) > DateTime.Now) + { + member.IsActive = true; + } + else + { + } + if (x.ServicePrincipalName != null && x.ServicePrincipalName.Length > 0) + { + member.IsService = true; + member.SPN = new List(x.ServicePrincipalName); + } + if ((x.UserAccountControl & 0x00000010) != 0) + { + member.IsLocked = true; + } + if ((x.UserAccountControl & 0x00010000) != 0) + { + member.DoesPwdNeverExpires = true; + } + if ((x.UserAccountControl & 0x00000020) != 0) + { + member.IsPwdNotRequired = true; + } + // this account is sensitive and cannot be delegated + if ((x.UserAccountControl & 0x100000) == 0) + { + member.CanBeDelegated = true; + } + if ((x.UserAccountControl & 0x40000) != 0) + { + member.SmartCardRequired = true; + } + } + return member; + } + + private SingleCompromiseGraphComputerMemberData BuildMembersComputer(ADItem x) + { + var member = new SingleCompromiseGraphComputerMemberData(); + member.Name = x.SAMAccountName; + member.DistinguishedName = x.DistinguishedName; + member.LastLogonTimestamp = x.LastLogonTimestamp; + if ((x.UserAccountControl & 0x00000002) != 0) + { + } + else + { + member.IsEnabled = true; + // last login since 6 months + if (x.LastLogonTimestamp.AddDays(6 * 31) > DateTime.Now) + { + member.IsActive = true; + } + else + { + } + if (x.ServicePrincipalName != null && x.ServicePrincipalName.Length > 0) + { + member.SPN = new List(x.ServicePrincipalName); + } + if ((x.UserAccountControl & 0x00000010) != 0) + { + member.IsLocked = true; + } + // this account is sensitive and cannot be delegated + if ((x.UserAccountControl & 0x100000) == 0) + { + member.CanBeDelegated = true; + } + } + return member; + } #region node relation links private void ComputeDistance(int startid, Dictionary> relations, Dictionary nodes) @@ -154,16 +671,59 @@ private void ComputeDistance(int startid, Dictionary> relati } } + private List RetrieveDirectUserNodes(int id, IEnumerable linkTypes) + { + List directUserNodes = new List(); + List input = new List() { id }; + List output = new List(); + var rootNode = storage.RetrieveNode(id); + foreach (string linkType in linkTypes) + { + if (rootNode.Type == linkType) + { + directUserNodes.Add(id); + break; + } + } + while (input.Count > 0) + { + List data = storage.SearchRelations(input, input); + foreach (Relation link in data) + { + if (link.Hint.Contains(RelationType.group_member.ToString()) || + link.Hint.Contains(RelationType.primary_group_member.ToString())) + { + output.Add(link.ToId); + if (!directUserNodes.Contains(link.ToId)) + { + var node = storage.RetrieveNode(link.ToId); + foreach (string linkType in linkTypes) + { + if (node.Type == linkType) + { + directUserNodes.Add(link.ToId); + break; + } + } + } + } + } + input.Clear(); + input.AddRange(output); + output.Clear(); + } + return directUserNodes; + } + private Dictionary> RetrieveLinks(int id, List nodesid) { Dictionary> output = new Dictionary>(); - List input = new List(); - input.Add(id); + List input = new List() { id }; nodesid.Add(id); int depth = 1; while (true) { - List data = storage.SearchRelations(input, nodesid, false); + List data = storage.SearchRelations(input, nodesid); if (data.Count > 0) { input.Clear(); @@ -177,7 +737,9 @@ private Dictionary> RetrieveLinks(int id, List nodesid) if (!nodesid.Contains(link.ToId)) { nodesid.Add(link.ToId); - input.Add(link.ToId); + var node = storage.RetrieveNode(link.ToId); + if (!IsStopNode(node)) + input.Add(link.ToId); } } if (nodesid.Count > MaxNodes) @@ -200,6 +762,28 @@ private Dictionary> RetrieveLinks(int id, List nodesid) return output; } + private bool IsStopNode(Node node) + { + if ((!String.IsNullOrEmpty(node.Sid) && stopNodes.Contains(node.Sid)) + || (node.ADItem != null && !String.IsNullOrEmpty(node.ADItem.DistinguishedName) && stopNodes.Contains(node.ADItem.DistinguishedName))) + { + return true; + } + return false; + } + + private void PrepareStopNodes(GraphObjectReference ObjectReference) + { + stopNodes.Clear(); + foreach (var typology in ObjectReference.Objects.Keys) + { + foreach (var obj in ObjectReference.Objects[typology]) + { + stopNodes.Add(obj.Name); + } + } + } + void SimplifyGraph(Dictionary nodes, Dictionary> links) { // remove file node from GPO when there is nothing else than files connected to @@ -294,10 +878,10 @@ void SimplifyGraph(Dictionary nodes, Dictionary> #region data file - private SingleCompromiseGraphData BuildSingleCompromiseGraphData(int rootNodeId, Dictionary nodes, Dictionary> links) + private SingleCompromiseGraphData BuildSingleCompromiseGraphData(int rootNodeId, Dictionary nodes, Dictionary> links, List directNodes) { var data = new SingleCompromiseGraphData(); - Dictionary idconversiontable = new Dictionary(); + var idconversiontable = new Dictionary(); // START OF NODES @@ -322,7 +906,7 @@ private SingleCompromiseGraphData BuildSingleCompromiseGraphData(int rootNodeId, continue; if (String.Equals(node.Type, "foreignsecurityprincipal", StringComparison.OrdinalIgnoreCase)) { - if (String.Equals(node.Shortname, "S-1-5-11", StringComparison.OrdinalIgnoreCase)) + if (String.Equals(node.Sid, "S-1-5-11", StringComparison.OrdinalIgnoreCase)) { data.Nodes.Add(new SingleCompromiseGraphNodeData() { @@ -331,12 +915,16 @@ private SingleCompromiseGraphData BuildSingleCompromiseGraphData(int rootNodeId, Type = node.Type, ShortName = "Authenticated Users", Distance = node.Distance, + Critical = true, + ADItem = node.ADItem, }); - data.UnusualGroup = true; + // unusual except when found on pre win2k group + if (!String.Equals(rootNode.Sid, "S-1-5-32-554", StringComparison.OrdinalIgnoreCase)) + data.CriticalObjectFound = true; idconversiontable[node.Id] = nodenumber++; continue; } - if (String.Equals(node.Shortname, "S-1-1-0", StringComparison.OrdinalIgnoreCase)) + if (String.Equals(node.Sid, "S-1-1-0", StringComparison.OrdinalIgnoreCase)) { data.Nodes.Add(new SingleCompromiseGraphNodeData() { @@ -345,12 +933,17 @@ private SingleCompromiseGraphData BuildSingleCompromiseGraphData(int rootNodeId, Type = node.Type, ShortName = "Everyone", Distance = node.Distance, + Critical = true, + ADItem = node.ADItem, }); - data.UnusualGroup = true; + data.CriticalObjectFound = true; idconversiontable[node.Id] = nodenumber++; continue; } } + bool domainUsersFound = (node.Type == "foreignsecurityprincipal" && node.Name.EndsWith("-513")); + if (domainUsersFound) + data.CriticalObjectFound = true; data.Nodes.Add(new SingleCompromiseGraphNodeData() { Id = nodenumber, @@ -358,6 +951,9 @@ private SingleCompromiseGraphData BuildSingleCompromiseGraphData(int rootNodeId, Type = node.Type, ShortName = node.Shortname, Distance = node.Distance, + Suspicious = (node.Type == "user" && !directNodes.Contains(node.Id)), + Critical = domainUsersFound, + ADItem = node.ADItem, }); idconversiontable[node.Id] = nodenumber++; } diff --git a/Graph/Rules/CompromiseGraphAnomalyIndirectCritical10.cs b/Graph/Rules/CompromiseGraphAnomalyIndirectCritical10.cs new file mode 100644 index 0000000..ed69ae6 --- /dev/null +++ b/Graph/Rules/CompromiseGraphAnomalyIndirectCritical10.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using PingCastle.Rules; +using PingCastle.Data; + +namespace PingCastle.Graph.Rules +{ + [RuleObjectiveAttribute("A-IndirectUser-Critical-10", RiskRuleCategory.Anomalies, RiskModelObjective.AnomalyAccessCritical)] + [RuleComputation(RuleComputationType.Objective, 70)] + public class CompromiseGraphAnomalyIndirectUserCritical10 : CompromiseGraphAnomalyIndirectUser + { + protected override int? AnalyzeDataNew(CompromiseGraphData compromiseGraphData) + { + return AnalyzeDataNew(compromiseGraphData, CompromiseGraphDataObjectRisk.Critical, 10); + } + } +} diff --git a/Graph/Rules/CompromiseGraphAnomalyIndirectCritical100.cs b/Graph/Rules/CompromiseGraphAnomalyIndirectCritical100.cs new file mode 100644 index 0000000..aff1a16 --- /dev/null +++ b/Graph/Rules/CompromiseGraphAnomalyIndirectCritical100.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using PingCastle.Rules; +using PingCastle.Data; + +namespace PingCastle.Graph.Rules +{ + [RuleObjectiveAttribute("A-IndirectUser-Critical-100", RiskRuleCategory.Anomalies, RiskModelObjective.AnomalyAccessCritical)] + [RuleComputation(RuleComputationType.Objective, 90)] + public class CompromiseGraphAnomalyIndirectUserCritical100 : CompromiseGraphAnomalyIndirectUser + { + protected override int? AnalyzeDataNew(CompromiseGraphData compromiseGraphData) + { + return AnalyzeDataNew(compromiseGraphData, CompromiseGraphDataObjectRisk.Critical, 100); + } + } +} diff --git a/Graph/Rules/CompromiseGraphAnomalyIndirectCritical50.cs b/Graph/Rules/CompromiseGraphAnomalyIndirectCritical50.cs new file mode 100644 index 0000000..176f521 --- /dev/null +++ b/Graph/Rules/CompromiseGraphAnomalyIndirectCritical50.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using PingCastle.Rules; +using PingCastle.Data; + +namespace PingCastle.Graph.Rules +{ + [RuleObjectiveAttribute("A-IndirectUser-Critical-50", RiskRuleCategory.Anomalies, RiskModelObjective.AnomalyAccessCritical)] + [RuleComputation(RuleComputationType.Objective, 80)] + public class CompromiseGraphAnomalyIndirectUserCritical50 : CompromiseGraphAnomalyIndirectUser + { + protected override int? AnalyzeDataNew(CompromiseGraphData compromiseGraphData) + { + return AnalyzeDataNew(compromiseGraphData, CompromiseGraphDataObjectRisk.Critical, 50); + } + } +} diff --git a/Graph/Rules/CompromiseGraphAnomalyIndirectCriticalAny.cs b/Graph/Rules/CompromiseGraphAnomalyIndirectCriticalAny.cs new file mode 100644 index 0000000..02d5fb0 --- /dev/null +++ b/Graph/Rules/CompromiseGraphAnomalyIndirectCriticalAny.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using PingCastle.Rules; +using PingCastle.Data; + +namespace PingCastle.Graph.Rules +{ + [RuleObjectiveAttribute("A-IndirectUser-Critical-Any", RiskRuleCategory.Anomalies, RiskModelObjective.AnomalyAccessCritical)] + [RuleComputation(RuleComputationType.Objective, 100)] + [RuleANSSI("R18", "subsubsection.3.3.2")] + public class CompromiseGraphAnomalyIndirectUserCriticalAny : CompromiseGraphAnomalyIndirectUser + { + protected override int? AnalyzeDataNew(CompromiseGraphData compromiseGraphData) + { + return AnalyzeDataNew(compromiseGraphData, CompromiseGraphDataObjectRisk.Critical, -1); + } + } +} diff --git a/Graph/Rules/CompromiseGraphAnomalyIndirectHigh10.cs b/Graph/Rules/CompromiseGraphAnomalyIndirectHigh10.cs new file mode 100644 index 0000000..d2655c3 --- /dev/null +++ b/Graph/Rules/CompromiseGraphAnomalyIndirectHigh10.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using PingCastle.Rules; +using PingCastle.Data; + +namespace PingCastle.Graph.Rules +{ + [RuleObjectiveAttribute("A-IndirectUser-High-10", RiskRuleCategory.Anomalies, RiskModelObjective.AnomalyAccessHigh)] + [RuleComputation(RuleComputationType.Objective, 60)] + public class CompromiseGraphAnomalyIndirectUserHigh10 : CompromiseGraphAnomalyIndirectUser + { + protected override int? AnalyzeDataNew(CompromiseGraphData compromiseGraphData) + { + return AnalyzeDataNew(compromiseGraphData, CompromiseGraphDataObjectRisk.High, 10); + } + } +} diff --git a/Graph/Rules/CompromiseGraphAnomalyIndirectHigh100.cs b/Graph/Rules/CompromiseGraphAnomalyIndirectHigh100.cs new file mode 100644 index 0000000..e06e639 --- /dev/null +++ b/Graph/Rules/CompromiseGraphAnomalyIndirectHigh100.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using PingCastle.Rules; +using PingCastle.Data; + +namespace PingCastle.Graph.Rules +{ + [RuleObjectiveAttribute("A-IndirectUser-High-100", RiskRuleCategory.Anomalies, RiskModelObjective.AnomalyAccessHigh)] + [RuleComputation(RuleComputationType.Objective, 80)] + public class CompromiseGraphAnomalyIndirectUserHigh100 : CompromiseGraphAnomalyIndirectUser + { + protected override int? AnalyzeDataNew(CompromiseGraphData compromiseGraphData) + { + return AnalyzeDataNew(compromiseGraphData, CompromiseGraphDataObjectRisk.High, 100); + } + } +} diff --git a/Graph/Rules/CompromiseGraphAnomalyIndirectHigh50.cs b/Graph/Rules/CompromiseGraphAnomalyIndirectHigh50.cs new file mode 100644 index 0000000..f94abe2 --- /dev/null +++ b/Graph/Rules/CompromiseGraphAnomalyIndirectHigh50.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using PingCastle.Rules; +using PingCastle.Data; + +namespace PingCastle.Graph.Rules +{ + [RuleObjectiveAttribute("A-IndirectUser-High-50", RiskRuleCategory.Anomalies, RiskModelObjective.AnomalyAccessHigh)] + [RuleComputation(RuleComputationType.Objective, 70)] + public class CompromiseGraphAnomalyIndirectUserHigh50 : CompromiseGraphAnomalyIndirectUser + { + protected override int? AnalyzeDataNew(CompromiseGraphData compromiseGraphData) + { + return AnalyzeDataNew(compromiseGraphData, CompromiseGraphDataObjectRisk.High, 50); + } + } +} diff --git a/Graph/Rules/CompromiseGraphAnomalyIndirectHighAny.cs b/Graph/Rules/CompromiseGraphAnomalyIndirectHighAny.cs new file mode 100644 index 0000000..f9ae4b8 --- /dev/null +++ b/Graph/Rules/CompromiseGraphAnomalyIndirectHighAny.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using PingCastle.Rules; +using PingCastle.Data; + +namespace PingCastle.Graph.Rules +{ + [RuleObjectiveAttribute("A-IndirectUser-High-Any", RiskRuleCategory.Anomalies, RiskModelObjective.AnomalyAccessHigh)] + [RuleComputation(RuleComputationType.Objective, 90)] + [RuleANSSI("R18", "subsubsection.3.3.2")] + public class CompromiseGraphAnomalyIndirectUserHighAny : CompromiseGraphAnomalyIndirectUser + { + protected override int? AnalyzeDataNew(CompromiseGraphData compromiseGraphData) + { + return AnalyzeDataNew(compromiseGraphData, CompromiseGraphDataObjectRisk.High, -1); + } + } +} diff --git a/Graph/Rules/CompromiseGraphAnomalyIndirectMedium10.cs b/Graph/Rules/CompromiseGraphAnomalyIndirectMedium10.cs new file mode 100644 index 0000000..af69dd1 --- /dev/null +++ b/Graph/Rules/CompromiseGraphAnomalyIndirectMedium10.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using PingCastle.Rules; +using PingCastle.Data; + +namespace PingCastle.Graph.Rules +{ + [RuleObjectiveAttribute("A-IndirectUser-Medium-10", RiskRuleCategory.Anomalies, RiskModelObjective.AnomalyAccessMedium)] + [RuleComputation(RuleComputationType.Objective, 50)] + public class CompromiseGraphAnomalyIndirectUserMedium10 : CompromiseGraphAnomalyIndirectUser + { + protected override int? AnalyzeDataNew(CompromiseGraphData compromiseGraphData) + { + return AnalyzeDataNew(compromiseGraphData, CompromiseGraphDataObjectRisk.Medium, 10); + } + } +} diff --git a/Graph/Rules/CompromiseGraphAnomalyIndirectMedium100.cs b/Graph/Rules/CompromiseGraphAnomalyIndirectMedium100.cs new file mode 100644 index 0000000..cf6e870 --- /dev/null +++ b/Graph/Rules/CompromiseGraphAnomalyIndirectMedium100.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using PingCastle.Rules; +using PingCastle.Data; + +namespace PingCastle.Graph.Rules +{ + [RuleObjectiveAttribute("A-IndirectUser-Medium-100", RiskRuleCategory.Anomalies, RiskModelObjective.AnomalyAccessMedium)] + [RuleComputation(RuleComputationType.Objective, 70)] + public class CompromiseGraphAnomalyIndirectUserMedium100 : CompromiseGraphAnomalyIndirectUser + { + protected override int? AnalyzeDataNew(CompromiseGraphData compromiseGraphData) + { + return AnalyzeDataNew(compromiseGraphData, CompromiseGraphDataObjectRisk.Medium, 100); + } + } +} diff --git a/Graph/Rules/CompromiseGraphAnomalyIndirectMedium50.cs b/Graph/Rules/CompromiseGraphAnomalyIndirectMedium50.cs new file mode 100644 index 0000000..1485cf5 --- /dev/null +++ b/Graph/Rules/CompromiseGraphAnomalyIndirectMedium50.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using PingCastle.Rules; +using PingCastle.Data; + +namespace PingCastle.Graph.Rules +{ + [RuleObjectiveAttribute("A-IndirectUser-Medium-50", RiskRuleCategory.Anomalies, RiskModelObjective.AnomalyAccessMedium)] + [RuleComputation(RuleComputationType.Objective, 60)] + public class CompromiseGraphAnomalyIndirectUserMedium50 : CompromiseGraphAnomalyIndirectUser + { + protected override int? AnalyzeDataNew(CompromiseGraphData compromiseGraphData) + { + return AnalyzeDataNew(compromiseGraphData, CompromiseGraphDataObjectRisk.Medium, 50); + } + } +} diff --git a/Graph/Rules/CompromiseGraphAnomalyIndirectMediumAny.cs b/Graph/Rules/CompromiseGraphAnomalyIndirectMediumAny.cs new file mode 100644 index 0000000..13c2d81 --- /dev/null +++ b/Graph/Rules/CompromiseGraphAnomalyIndirectMediumAny.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using PingCastle.Rules; +using PingCastle.Data; + +namespace PingCastle.Graph.Rules +{ + [RuleObjectiveAttribute("A-IndirectUser-Medium-Any", RiskRuleCategory.Anomalies, RiskModelObjective.AnomalyAccessMedium)] + [RuleComputation(RuleComputationType.Objective, 80)] + [RuleANSSI("R18", "subsubsection.3.3.2")] + public class CompromiseGraphAnomalyIndirectUserMediumAny : CompromiseGraphAnomalyIndirectUser + { + protected override int? AnalyzeDataNew(CompromiseGraphData compromiseGraphData) + { + return AnalyzeDataNew(compromiseGraphData, CompromiseGraphDataObjectRisk.Medium, -1); + } + } +} diff --git a/Graph/Rules/CompromiseGraphAnomalyIndirectUser.cs b/Graph/Rules/CompromiseGraphAnomalyIndirectUser.cs new file mode 100644 index 0000000..827ec07 --- /dev/null +++ b/Graph/Rules/CompromiseGraphAnomalyIndirectUser.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using PingCastle.Rules; +using PingCastle.Data; + +namespace PingCastle.Graph.Rules +{ + [RuleObjectiveAttribute("A-IndirectUser", RiskRuleCategory.Anomalies, RiskModelObjective.AnomalyAccessCritical)] + [RuleComputation(RuleComputationType.Objective, 100)] + public abstract class CompromiseGraphAnomalyIndirectUser : CompromiseGraphRule + { + protected int? AnalyzeDataNew(CompromiseGraphData compromiseGraphData, CompromiseGraphDataObjectRisk risk, int trigger) + { + foreach (var analysis in compromiseGraphData.AnomalyAnalysis) + { + if (analysis.ObjectRisk != risk) + { + continue; + } + if (trigger == -1) + { + if (analysis.CriticalObjectFound) + return 1; + } + else if (analysis.MaximumIndirectNumber > trigger) + { + return 1; + } + } + return -1; + } + } +} diff --git a/Graph/Rules/CompromiseGraphPrivilegedOperatorsEmpty.cs b/Graph/Rules/CompromiseGraphPrivilegedOperatorsEmpty.cs new file mode 100644 index 0000000..a3665f2 --- /dev/null +++ b/Graph/Rules/CompromiseGraphPrivilegedOperatorsEmpty.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Text; +using PingCastle.Rules; +using PingCastle.Data; + +namespace PingCastle.Graph.Rules +{ + [RuleObjectiveAttribute("P-OperatorsEmpty", RiskRuleCategory.PrivilegedAccounts, RiskModelObjective.PrivilegedBestPractices)] + [RuleComputation(RuleComputationType.Objective, 25)] + [RuleANSSI("R27", "subsection.3.5")] + public class CompromiseGraphPrivilegedOperatorsEmpty : CompromiseGraphRule + { + protected override int? AnalyzeDataNew(CompromiseGraphData compromiseGraphData) + { + foreach (var single in compromiseGraphData.Data) + { + if (single.Typology != CompromiseGraphDataTypology.PrivilegedAccount) + continue; + if (single.Name != "S-1-5-32-548" && single.Name != "S-1-5-32-549") + continue; + foreach (var obj in single.Nodes) + { + if (obj.Type != "user") + continue; + if (obj.Suspicious) + continue; + return 1; + } + } + return -1; + } + } +} diff --git a/Graph/Rules/CompromiseGraphRule.cs b/Graph/Rules/CompromiseGraphRule.cs new file mode 100644 index 0000000..69d2e9b --- /dev/null +++ b/Graph/Rules/CompromiseGraphRule.cs @@ -0,0 +1,61 @@ +using PingCastle.Data; +using PingCastle.Rules; +using System; +using System.Collections.Generic; +using System.Resources; +using System.Text; + +namespace PingCastle.Graph.Rules +{ + public abstract class CompromiseGraphRule : RuleBase + { + public Dictionary ImpactedGraph { get; set; } + + private readonly string GraphRationale; + + public CompromiseGraphRule() : base() + { + ImpactedGraph = new Dictionary(); + var resourceKey = RiskId.Replace('-', '_').Replace('$', '_'); + GraphRationale = ResourceManager.GetString(resourceKey + "_GraphRationale"); + } + + public void AddGraph(SingleCompromiseGraphData graphData) + { + if (!ImpactedGraph.ContainsKey(graphData.Name)) + { + var detail = new CompromiseGraphRuleDetail(); + detail.Details = new List(); + ImpactedGraph.Add(graphData.Name, detail); + } + string ruleDetail = String.Format("{1} ({0})", graphData.Name, graphData.Description); + if (!Details.Contains(ruleDetail)) + Details.Add(ruleDetail); + } + + public void AddGraphRawDetail(SingleCompromiseGraphData graphData, params object[] data) + { + AddGraph(graphData); + ImpactedGraph[graphData.Name].Details.Add(String.Format(DetailFormatString, data)); + + } + + protected override void UpdateLabelsAfterMatch(int valueReturnedByAnalysis, RuleComputationAttribute computation) + { + base.UpdateLabelsAfterMatch(valueReturnedByAnalysis, computation); + if (GraphRationale != null) + { + foreach (var detail in ImpactedGraph.Values) + { + detail.Rationale = GraphRationale.Replace("{count}", detail.Details.Count.ToString()); + } + } + } + } + + public class CompromiseGraphRuleDetail + { + public string Rationale { get; set; } + public List Details { get; set; } + } +} diff --git a/Graph/Rules/CompromiseGraphStalePermissionsCleanup.cs b/Graph/Rules/CompromiseGraphStalePermissionsCleanup.cs new file mode 100644 index 0000000..39df4be --- /dev/null +++ b/Graph/Rules/CompromiseGraphStalePermissionsCleanup.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Text; +using PingCastle.Rules; +using PingCastle.Data; + +namespace PingCastle.Graph.Rules +{ + [RuleObjectiveAttribute("S-PermissionsCleanup", RiskRuleCategory.StaleObjects, RiskModelObjective.StaleObjectsHouseKeeping)] + [RuleComputation(RuleComputationType.Objective, 25)] + public class CompromiseGraphStalePermissionsCleanup : CompromiseGraphRule + { + protected override int? AnalyzeDataNew(CompromiseGraphData compromiseGraphData) + { + foreach (var single in compromiseGraphData.Data) + { + if (single.Typology != CompromiseGraphDataTypology.PrivilegedAccount && single.Typology != CompromiseGraphDataTypology.PrivilegedAccount) + continue; + if (single.DeletedObjects != null && single.DeletedObjects.Count > 0) + return 1; + + } + return -1; + } + } +} diff --git a/Graph/Rules/CompromiseGraphTrustChildDomainHasPermission.cs b/Graph/Rules/CompromiseGraphTrustChildDomainHasPermission.cs new file mode 100644 index 0000000..ba78d98 --- /dev/null +++ b/Graph/Rules/CompromiseGraphTrustChildDomainHasPermission.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using PingCastle.Rules; +using PingCastle.Data; + +namespace PingCastle.Graph.Rules +{ + [RuleObjectiveAttribute("T-ChildDomainHasPermission", RiskRuleCategory.Trusts, RiskModelObjective.TrustBestPractices)] + [RuleComputation(RuleComputationType.Objective, 25)] + public class CompromiseGraphTrustChildDomainHasPermission : CompromiseGraphRule + { + protected override int? AnalyzeDataNew(CompromiseGraphData compromiseGraphData) + { + foreach (var single in compromiseGraphData.Dependancies) + { + + } + return -1; + } + } +} diff --git a/Graph/Rules/CompromiseGraphTrustMoreThanOneDomainControlCritical.cs b/Graph/Rules/CompromiseGraphTrustMoreThanOneDomainControlCritical.cs new file mode 100644 index 0000000..51507f2 --- /dev/null +++ b/Graph/Rules/CompromiseGraphTrustMoreThanOneDomainControlCritical.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using PingCastle.Rules; +using PingCastle.Data; + +namespace PingCastle.Graph.Rules +{ + [RuleObjectiveAttribute("T-OneDomainControlCritical", RiskRuleCategory.Trusts, RiskModelObjective.TrustPermeability)] + [RuleComputation(RuleComputationType.Objective, 100)] + public class CompromiseGraphTrustMoreThanOneDomainControlCritical : CompromiseGraphTrustOneDomainControlCritical + { + protected override int? AnalyzeDataNew(CompromiseGraphData compromiseGraphData) + { + return AnalyzeDataNew(compromiseGraphData, 1); + } + } +} diff --git a/Graph/Rules/CompromiseGraphTrustOneDomainControlCritical.cs b/Graph/Rules/CompromiseGraphTrustOneDomainControlCritical.cs new file mode 100644 index 0000000..dced62b --- /dev/null +++ b/Graph/Rules/CompromiseGraphTrustOneDomainControlCritical.cs @@ -0,0 +1,46 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using PingCastle.Rules; +using PingCastle.Data; + +namespace PingCastle.Graph.Rules +{ + [RuleObjectiveAttribute("T-MoreThanOneDomainControlCritical", RiskRuleCategory.Trusts, RiskModelObjective.TrustPermeability)] + [RuleComputation(RuleComputationType.Objective, 85)] + public class CompromiseGraphTrustOneDomainControlCritical : CompromiseGraphRule + { + + protected override int? AnalyzeDataNew(CompromiseGraphData compromiseGraphData) + { + return AnalyzeDataNew(compromiseGraphData, 0); + } + + protected int? AnalyzeDataNew(CompromiseGraphData compromiseGraphData, int trigger) + { + int domainCount = 0; + foreach (var single in compromiseGraphData.Dependancies) + { + foreach (var detail in single.Details) + { + if (detail.Typology == CompromiseGraphDataTypology.PrivilegedAccount || + detail.Typology == CompromiseGraphDataTypology.Infrastructure) + { + domainCount++; + break; + } + } + } + if (domainCount > trigger) + return 1; + return -1; + } + } +} diff --git a/Graph/Rules/CompromiseGraphTrustOneDomainControlUserDefined.cs b/Graph/Rules/CompromiseGraphTrustOneDomainControlUserDefined.cs new file mode 100644 index 0000000..61f2e0e --- /dev/null +++ b/Graph/Rules/CompromiseGraphTrustOneDomainControlUserDefined.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using PingCastle.Rules; +using PingCastle.Data; + +namespace PingCastle.Graph.Rules +{ + [RuleObjectiveAttribute("T-OneDomainControlUserDefined", RiskRuleCategory.Trusts, RiskModelObjective.TrustPermeability)] + [RuleComputation(RuleComputationType.Objective, 20)] + public class CompromiseGraphTrustOneDomainControlUserDefined : CompromiseGraphRule + { + protected override int? AnalyzeDataNew(CompromiseGraphData compromiseGraphData) + { + foreach (var single in compromiseGraphData.Dependancies) + { + foreach (var detail in single.Details) + { + if (detail.Typology == CompromiseGraphDataTypology.UserDefined) + { + return 1; + } + } + } + return -1; + } + } +} diff --git a/Graph/Rules/RuleDescription.resx b/Graph/Rules/RuleDescription.resx new file mode 100644 index 0000000..51358e3 --- /dev/null +++ b/Graph/Rules/RuleDescription.resx @@ -0,0 +1,243 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The purpose is to ensure that the accounts which can have access to the resource are the member of it or being part of the administrator group. + + + {0} + + + + + + {count} account(s) can take control of this resource without official permission + + + {count} critical object(s) can be put under control without official permission + + + Review the account list. Then you should replace the account by a delegation group or an admin group. + + + A member of a group has the same granted permission that the group. But a member of a helpdesk group can take control of a member of this group by resetting its password, it can have the same permission than this group without being a member of it. PingCastle checks for similar techniques to identify such accounts. + + + Check for accounts having indirect control to the resource + + + The purpose is to ensure that the operator groups which can have indirect control to the domain are empty + + + + + + + + + {count} operator group(s) are not empty + + + It is recommended to have these groups empty. Assign administrators into administrators group. Other accounts should have proper delegation rights in OU or in the scope they are managing. + + + Operator groups (account operators, server operators, ...) can take indirectly the control of the domain. Indeed these groups have write access to critical resources of the domain. + + + Check that operators group are empty + + + The group is not empty while it is recommended to be empty + + + The purpose is to ensure that no group having a large number of users can take control of an asset + + + + + + + + + Well known groups such as Everyone can take control of this asset + + + Well known groups such as Everyone can take control of {count} asset(s) + + + It is recommended to analyze the control path giving access to these assets and fix it. Also such generic group should not be granted access, access control lists should be reviewed to remove them. + + + Users belongs to well known groups such as Everyone, Authenticated Users or Domain Users. PingCastle has found that one of this group can take control of at least one asset. Except for Pre-Windows 2000 Compatible Access where Authenticated Users is allowed, it should not happen. + + + Check that any users cannot take control of an asset + + + No domain can take control of an admin or critical object + + + No more than 1 domain can take control of an admin or critical object + + + No domain can take control of a user defined object + + + No child domain of a forest should have permission on this domain + + + "Any users" have no direct or indirect access to critical objects + + + No more than 100 users have no indirect access to critical objects + + + No more than 50 users have no indirect access to critical objects + + + No more than 10 users have no indirect access to critical objects + + + "Any users" have no direct or indirect access to high value objects + + + No more than 100 users have no indirect access to high value objects + + + No more than 50 users have no indirect access to high value objects + + + No more than 10 users have no indirect access to high value objects + + + "Any users" have no direct or indirect access to medium value objects + + + No more than 100 users have no indirect access to medium value objects + + + No more than 50 users have no indirect access to medium value objects + + + No more than 10 users have no indirect access to medium value objects + + + No object should have on his control path a permission related to deleted objects + + \ No newline at end of file diff --git a/Healthcheck/ADModel.cs b/Healthcheck/ADModel.cs index 09a9062..16e8389 100644 --- a/Healthcheck/ADModel.cs +++ b/Healthcheck/ADModel.cs @@ -4,6 +4,7 @@ // // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. // +using PingCastle.Data; using System; using System.Collections.Generic; using System.Diagnostics; @@ -102,12 +103,12 @@ public GraphNode Locate(DomainKey domain) public GraphNode GetDomain(string center) { - DomainKey key = new DomainKey(center, null); + DomainKey key = new DomainKey(center, null, null); return Locate(key); } // sometimes we have only the netbios name. Try to find if we know the FQDN - private static void EnrichDomainInfo(HealthcheckDataCollection consolidation, HealthCheckTrustDomainInfoData di) + private static void EnrichDomainInfo(PingCastleReportCollection consolidation, HealthCheckTrustDomainInfoData di) { bool enriched = false; // search direct report @@ -151,7 +152,7 @@ private static void EnrichDomainInfo(HealthcheckDataCollection consolidation, He } } - static public GraphNodeCollection BuildModel(HealthcheckDataCollection consolidation, OwnerInformationReferences EntityData) + static public GraphNodeCollection BuildModel(PingCastleReportCollection consolidation, OwnerInformationReferences EntityData) { GraphNodeCollection nodes = new GraphNodeCollection(); // build links based on the most to the less reliable information @@ -217,30 +218,34 @@ static public GraphNodeCollection BuildModel(HealthcheckDataCollection consolida EnrichDomainInfo(consolidation, di); } // if no information was given (only Netbios name!) fallback to a forest trust - if (String.IsNullOrEmpty(di.ForestName)) - { - di.ForestName = di.DnsName; - } - // ignore the domain if the forest trust is known (information should be already there) - if (consolidation.GetDomain(di.Forest) != null) - continue; - - // add the forest trust if needed - GraphNode remoteForestRoot = nodes.CreateNodeIfNeeded(ref nodeNumber, di.Forest, di.ForestNetbios, data.GenerationDate); - remoteForestRoot.SetForest(remoteForestRoot.Domain); - - // add the forest root if needed - GraphNode myForestRoot = nodes.CreateNodeIfNeeded(ref nodeNumber, data.Forest, null, data.GenerationDate); - myForestRoot.LinkTwoForests(remoteForestRoot); - myForestRoot.SetForest(myForestRoot.Domain); - // add the trust if the domain is a child of the forest) - // (ignore the trust if forest root = trust) - if (di.ForestName.Equals(di.DnsName, StringComparison.InvariantCultureIgnoreCase)) - continue; - - GraphNode childDomain = nodes.CreateNodeIfNeeded(ref nodeNumber, di.Domain, di.NetbiosName, data.GenerationDate); - remoteForestRoot.LinkInsideAForest(childDomain); - childDomain.SetForest(remoteForestRoot.Domain); + if (String.IsNullOrEmpty(di.ForestName) || di.ForestName == di.DnsName) + { + GraphNode childDomain = nodes.CreateNodeIfNeeded(ref nodeNumber, di.Domain, di.NetbiosName, data.GenerationDate); + GraphNode myForestRoot = nodes.CreateNodeIfNeeded(ref nodeNumber, data.Forest, null, data.GenerationDate); + myForestRoot.LinkTwoForests(childDomain); + myForestRoot.SetForest(myForestRoot.Domain); + } + else + { + // ignore the domain if the forest trust is known (information should be already there) + if (consolidation.GetDomain(di.Forest) != null) + continue; + + // add the forest trust if needed + GraphNode remoteForestRoot = nodes.CreateNodeIfNeeded(ref nodeNumber, di.Forest, di.ForestNetbios, data.GenerationDate); + remoteForestRoot.SetForest(remoteForestRoot.Domain); + + // add the forest root if needed + GraphNode myForestRoot = nodes.CreateNodeIfNeeded(ref nodeNumber, data.Forest, null, data.GenerationDate); + myForestRoot.LinkTwoForests(remoteForestRoot); + myForestRoot.SetForest(myForestRoot.Domain); + // add the trust if the domain is a child of the forest) + // (ignore the trust if forest root = trust) + + GraphNode childDomain = nodes.CreateNodeIfNeeded(ref nodeNumber, di.Domain, di.NetbiosName, data.GenerationDate); + remoteForestRoot.LinkInsideAForest(childDomain); + childDomain.SetForest(remoteForestRoot.Domain); + } } } Trace.WriteLine("enrich forest information"); diff --git a/Healthcheck/HealthCheckEncryption.cs b/Healthcheck/HealthCheckEncryption.cs index 616d7b1..987a66c 100644 --- a/Healthcheck/HealthCheckEncryption.cs +++ b/Healthcheck/HealthCheckEncryption.cs @@ -14,210 +14,210 @@ namespace ADSecurityHealthCheck.Healthcheck { - internal class EncryptionSettings : PingCastle.Healthcheck.EncryptionSettings - { - } + internal class EncryptionSettings : PingCastle.Healthcheck.EncryptionSettings + { + } } namespace PingCastle.Healthcheck { - internal class EncryptionSettings : ConfigurationSection - { - //public static EncryptionSettings settings = - - static EncryptionSettings cachedSettings = null; - public static EncryptionSettings GetEncryptionSettings() - { - if (cachedSettings == null) - cachedSettings = ConfigurationManager.GetSection("encryptionSettings") as EncryptionSettings; - return cachedSettings; - } - - - [ConfigurationProperty("RSAKeys", IsRequired = false)] - public ConfigElementsCollection RSAKeys - { - get - { - return base["RSAKeys"] as ConfigElementsCollection; - } - } - - [ConfigurationProperty("encryptionKey", IsRequired = false)] - public string EncryptionKey - { - get - { - return base["encryptionKey"] as string; - } - set - { - base["encryptionKey"] = value; - } - } - - } - - [ConfigurationCollection(typeof(KeySettings), AddItemName = "KeySettings")] - public class ConfigElementsCollection : ConfigurationElementCollection - { - public ConfigElementsCollection() - { - - } - - public KeySettings this[int index] - { - get { return (KeySettings)BaseGet(index); } - set - { - if (BaseGet(index) != null) - { - BaseRemoveAt(index); - } - BaseAdd(index, value); - } - } - - public void Add(KeySettings pluginConfig) - { - BaseAdd(pluginConfig); - } - - public void Clear() - { - BaseClear(); - } - - protected override ConfigurationElement CreateNewElement() - { - return new KeySettings(); - } - - protected override object GetElementKey(ConfigurationElement element) - { - return ((KeySettings)element).Name; - } - - public void Remove(KeySettings pluginConfig) - { - BaseRemove(pluginConfig.Name); - } - - public void RemoveAt(int index) - { - BaseRemoveAt(index); - } - - public void Remove(string name) - { - BaseRemove(name); - } - - } - - public class KeySettings : ConfigurationElement - { - - [ConfigurationProperty("name", IsKey = true, IsRequired = true)] - public string Name - { - get - { - return base["name"] as string; - } - set - { - base["name"] = value; - } - } - - [ConfigurationProperty("publicKey", IsRequired = false)] - public string PublicKey - { - get - { - return base["publicKey"] as string; - } - set - { - base["publicKey"] = value; - } - } - - [ConfigurationProperty("privateKey", IsRequired = false)] - public string PrivateKey - { - get - { - return base["privateKey"] as string; - } - set - { - base["privateKey"] = value; - } - } - - } - - public class HealthCheckEncryption - { - public static RSA GetRSAEncryptionKey() - { - EncryptionSettings settings = EncryptionSettings.GetEncryptionSettings(); - string EncryptionKey = settings.EncryptionKey; - if (String.IsNullOrEmpty(EncryptionKey)) - { - foreach (KeySettings keyinfo in settings.RSAKeys) - { - if (!String.IsNullOrEmpty(keyinfo.PublicKey)) - { - RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(); - RSA.FromXmlString(keyinfo.PublicKey); - return RSA; - } - if (!String.IsNullOrEmpty(keyinfo.PrivateKey)) - { - RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(); - RSA.FromXmlString(keyinfo.PrivateKey); - return RSA; - } - } - throw new ApplicationException("No encryption key set in config file"); - } - else - { - foreach (KeySettings keyinfo in settings.RSAKeys) - { - if (keyinfo.Name == EncryptionKey) - { - RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(); - if (!String.IsNullOrEmpty(keyinfo.PublicKey)) - RSA.FromXmlString(keyinfo.PublicKey); - else if (!String.IsNullOrEmpty(keyinfo.PrivateKey)) - RSA.FromXmlString(keyinfo.PrivateKey); - else - throw new ApplicationException(@"The container """ + EncryptionKey + @""" does not contain a public or a private key"); - return RSA; - } - } - } - throw new ApplicationException("Encryption key not found (name:" + EncryptionKey + ")"); - } - - static string XmlEscape(string unescaped) - { - XmlDocument doc = new XmlDocument(); - XmlNode node = doc.CreateElement("root"); - node.InnerText = unescaped; - return node.InnerXml; - } - - public static void GenerateRSAKey() - { - RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(); - Console.WriteLine("Public Key (used on the encryption side):"); - Console.WriteLine(@" + internal class EncryptionSettings : ConfigurationSection + { + //public static EncryptionSettings settings = + + static EncryptionSettings cachedSettings = null; + public static EncryptionSettings GetEncryptionSettings() + { + if (cachedSettings == null) + cachedSettings = ConfigurationManager.GetSection("encryptionSettings") as EncryptionSettings; + return cachedSettings; + } + + + [ConfigurationProperty("RSAKeys", IsRequired = false)] + public ConfigElementsCollection RSAKeys + { + get + { + return base["RSAKeys"] as ConfigElementsCollection; + } + } + + [ConfigurationProperty("encryptionKey", IsRequired = false)] + public string EncryptionKey + { + get + { + return base["encryptionKey"] as string; + } + set + { + base["encryptionKey"] = value; + } + } + + } + + [ConfigurationCollection(typeof(KeySettings), AddItemName = "KeySettings")] + public class ConfigElementsCollection : ConfigurationElementCollection + { + public ConfigElementsCollection() + { + + } + + public KeySettings this[int index] + { + get { return (KeySettings)BaseGet(index); } + set + { + if (BaseGet(index) != null) + { + BaseRemoveAt(index); + } + BaseAdd(index, value); + } + } + + public void Add(KeySettings pluginConfig) + { + BaseAdd(pluginConfig); + } + + public void Clear() + { + BaseClear(); + } + + protected override ConfigurationElement CreateNewElement() + { + return new KeySettings(); + } + + protected override object GetElementKey(ConfigurationElement element) + { + return ((KeySettings)element).Name; + } + + public void Remove(KeySettings pluginConfig) + { + BaseRemove(pluginConfig.Name); + } + + public void RemoveAt(int index) + { + BaseRemoveAt(index); + } + + public void Remove(string name) + { + BaseRemove(name); + } + + } + + public class KeySettings : ConfigurationElement + { + + [ConfigurationProperty("name", IsKey = true, IsRequired = true)] + public string Name + { + get + { + return base["name"] as string; + } + set + { + base["name"] = value; + } + } + + [ConfigurationProperty("publicKey", IsRequired = false)] + public string PublicKey + { + get + { + return base["publicKey"] as string; + } + set + { + base["publicKey"] = value; + } + } + + [ConfigurationProperty("privateKey", IsRequired = false)] + public string PrivateKey + { + get + { + return base["privateKey"] as string; + } + set + { + base["privateKey"] = value; + } + } + + } + + public class HealthCheckEncryption + { + public static RSA GetRSAEncryptionKey() + { + EncryptionSettings settings = EncryptionSettings.GetEncryptionSettings(); + string EncryptionKey = settings.EncryptionKey; + if (String.IsNullOrEmpty(EncryptionKey)) + { + foreach (KeySettings keyinfo in settings.RSAKeys) + { + if (!String.IsNullOrEmpty(keyinfo.PublicKey)) + { + RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(); + RSAKeyExtensions.FromXmlStringDotNetCore2(RSA, keyinfo.PublicKey); + return RSA; + } + if (!String.IsNullOrEmpty(keyinfo.PrivateKey)) + { + RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(); + RSAKeyExtensions.FromXmlStringDotNetCore2(RSA, keyinfo.PrivateKey); + return RSA; + } + } + throw new PingCastleException("No encryption key set in config file"); + } + else + { + foreach (KeySettings keyinfo in settings.RSAKeys) + { + if (keyinfo.Name == EncryptionKey) + { + RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(); + if (!String.IsNullOrEmpty(keyinfo.PublicKey)) + RSAKeyExtensions.FromXmlStringDotNetCore2(RSA, keyinfo.PublicKey); + else if (!String.IsNullOrEmpty(keyinfo.PrivateKey)) + RSAKeyExtensions.FromXmlStringDotNetCore2(RSA, keyinfo.PrivateKey); + else + throw new PingCastleException(@"The container """ + EncryptionKey + @""" does not contain a public or a private key"); + return RSA; + } + } + } + throw new PingCastleException("Encryption key not found (name:" + EncryptionKey + ")"); + } + + static string XmlEscape(string unescaped) + { + XmlDocument doc = new XmlDocument(); + XmlNode node = doc.CreateElement("root"); + node.InnerText = unescaped; + return node.InnerXml; + } + + public static void GenerateRSAKey() + { + RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(); + Console.WriteLine("Public Key (used on the encryption side):"); + Console.WriteLine(@" @@ -225,44 +225,99 @@ public static void GenerateRSAKey() "); - Console.WriteLine("Private Key (used on the decryption side):"); - Console.WriteLine(@" + Console.WriteLine("Private Key (used on the decryption side):"); + Console.WriteLine(@" "); - Console.WriteLine("Done"); - } - - public static List GetAllPrivateKeys() - { - List output = new List(); - EncryptionSettings settings = EncryptionSettings.GetEncryptionSettings(); - if (settings == null) - { - Trace.WriteLine("No encryption setting found in config file"); - return output; - } - foreach (KeySettings keyinfo in settings.RSAKeys) - { - if (!String.IsNullOrEmpty(keyinfo.PrivateKey)) - { + Console.WriteLine("Done"); + } + + public static List GetAllPrivateKeys() + { + List output = new List(); + EncryptionSettings settings = EncryptionSettings.GetEncryptionSettings(); + if (settings == null) + { + Trace.WriteLine("No encryption setting found in config file"); + return output; + } + foreach (KeySettings keyinfo in settings.RSAKeys) + { + if (!String.IsNullOrEmpty(keyinfo.PrivateKey)) + { Trace.WriteLine("Loading key " + keyinfo.Name); - RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(); + RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(); try { - RSA.FromXmlString(keyinfo.PrivateKey); + RSAKeyExtensions.FromXmlStringDotNetCore2(RSA, keyinfo.PrivateKey); } catch (Exception ex) { - throw new ApplicationException("Unable to load the key \"" + keyinfo.Name + "\"", ex); + throw new PingCastleException("Unable to load the key \"" + keyinfo.Name + "\"", ex); } - output.Add(RSA); - } - } - return output; - } - } + output.Add(RSA); + } + } + return output; + } + } + + internal static class RSAKeyExtensions + { + + #region XML + + public static void FromXmlStringDotNetCore2(RSA rsa, string xmlString) + { + RSAParameters parameters = new RSAParameters(); + + XmlDocument xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(xmlString); + + if (xmlDoc.DocumentElement.Name.Equals("RSAKeyValue")) + { + foreach (XmlNode node in xmlDoc.DocumentElement.ChildNodes) + { + switch (node.Name) + { + case "Modulus": parameters.Modulus = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break; + case "Exponent": parameters.Exponent = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break; + case "P": parameters.P = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break; + case "Q": parameters.Q = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break; + case "DP": parameters.DP = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break; + case "DQ": parameters.DQ = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break; + case "InverseQ": parameters.InverseQ = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break; + case "D": parameters.D = (string.IsNullOrEmpty(node.InnerText) ? null : Convert.FromBase64String(node.InnerText)); break; + } + } + } + else + { + throw new Exception("Invalid XML RSA key."); + } + + rsa.ImportParameters(parameters); + } + + public static string ToXmlStringDotNetCore2(RSA rsa, bool includePrivateParameters) + { + RSAParameters parameters = rsa.ExportParameters(includePrivateParameters); + + return string.Format("{0}{1}

{2}

{3}{4}{5}{6}{7}
", + parameters.Modulus != null ? Convert.ToBase64String(parameters.Modulus) : null, + parameters.Exponent != null ? Convert.ToBase64String(parameters.Exponent) : null, + parameters.P != null ? Convert.ToBase64String(parameters.P) : null, + parameters.Q != null ? Convert.ToBase64String(parameters.Q) : null, + parameters.DP != null ? Convert.ToBase64String(parameters.DP) : null, + parameters.DQ != null ? Convert.ToBase64String(parameters.DQ) : null, + parameters.InverseQ != null ? Convert.ToBase64String(parameters.InverseQ) : null, + parameters.D != null ? Convert.ToBase64String(parameters.D) : null); + } + + #endregion + } } diff --git a/Healthcheck/HealthcheckAnalyzer.cs b/Healthcheck/HealthcheckAnalyzer.cs index 0fa9f99..fe8b7a9 100644 --- a/Healthcheck/HealthcheckAnalyzer.cs +++ b/Healthcheck/HealthcheckAnalyzer.cs @@ -25,25 +25,21 @@ using System.Net.NetworkInformation; using PingCastle.misc; using System.Security.Cryptography.X509Certificates; -using PingCastle.NullSession; +using PingCastle.RPC; using System.Security.Cryptography.Xml; using System.Runtime.InteropServices; using PingCastle.Scanners; using System.DirectoryServices.ActiveDirectory; using System.Security.Permissions; +using PingCastle.Rules; +using PingCastle.Data; namespace PingCastle.Healthcheck { - public class HealthcheckAnalyzer + public class HealthcheckAnalyzer : IPingCastleAnalyzer { public static bool SkipNullSession { get; set; } - public bool AnalyzeReachableDomains { get; set; } - public HealthcheckData healthcheckData { get; set; } - - - List Exploration; - - public int NumberOfDepthForSplit { get; set; } + HealthcheckData healthcheckData; public HealthcheckAnalyzer() { @@ -64,28 +60,29 @@ private void DisplayAdvancement(string data) Trace.WriteLine(value); } - public void GenerateCartoReport(string server, int port, NetworkCredential credential) - { - healthcheckData = new HealthcheckData(); - ADDomainInfo domainInfo = null; - using (ADWebService adws = new ADWebService(server, port, credential)) - { - domainInfo = adws.DomainInfo; - GenerateGeneralData(domainInfo, adws); - GenerateTrustData(domainInfo, adws); - if (AnalyzeReachableDomains) - { - GenerateReachableTrustData(domainInfo, adws); - } - } - } + public HealthcheckData GenerateCartoReport(string server, int port, NetworkCredential credential, bool AnalyzeReachableDomains) + { + healthcheckData = new HealthcheckData(); + ADDomainInfo domainInfo = null; + using (ADWebService adws = new ADWebService(server, port, credential)) + { + domainInfo = adws.DomainInfo; + GenerateGeneralData(domainInfo, adws); + GenerateTrustData(domainInfo, adws); + if (AnalyzeReachableDomains) + { + GenerateReachableTrustData(domainInfo, adws); + } + } + return healthcheckData; + } - public void GenerateReport(string server, int port, NetworkCredential credential) + public HealthcheckData PerformAnalyze(PingCastleAnalyzerParameters parameters) { - healthcheckData = new HealthcheckData(); + healthcheckData = new HealthcheckData(); ADDomainInfo domainInfo = null; - DisplayAdvancement("Getting domain information"); - using (ADWebService adws = new ADWebService(server, port, credential)) + DisplayAdvancement("Getting domain information (" + parameters.Server + ")"); + using (ADWebService adws = new ADWebService(parameters.Server, parameters.Port, parameters.Credential)) { domainInfo = adws.DomainInfo; if (adws.useLdap) @@ -103,7 +100,7 @@ public void GenerateReport(string server, int port, NetworkCredential credential GenerateComputerData(domainInfo, adws); DisplayAdvancement("Gathering trust data"); GenerateTrustData(domainInfo, adws); - if (AnalyzeReachableDomains) + if (parameters.PerformExtendedTrustDiscovery) { DisplayAdvancement("Gathering reachable domains data"); GenerateReachableTrustData(domainInfo, adws); @@ -115,8 +112,8 @@ public void GenerateReport(string server, int port, NetworkCredential credential //DisplayAdvancement("Gathering topology data"); //GenerateTopologyData(domainInfo, adws); DisplayAdvancement("Gathering gpo data"); - GenerateGPOData(domainInfo, adws, credential); - GeneratePSOData(domainInfo, adws, credential); + GenerateGPOData(domainInfo, adws, parameters.Credential); + GeneratePSOData(domainInfo, adws, parameters.Credential); DisplayAdvancement("Gathering anomaly data"); GenerateAnomalies(domainInfo, adws); DisplayAdvancement("Gathering domain controller data" + (SkipNullSession?null:" (including null session)")); @@ -125,10 +122,21 @@ public void GenerateReport(string server, int port, NetworkCredential credential GenerateNetworkData(domainInfo, adws); } DisplayAdvancement("Computing risks"); - HealthcheckRules rules = new HealthcheckRules(); - rules.ComputeRiskRules(healthcheckData); + var rules = new RuleSet(); + healthcheckData.RiskRules = new List(); + foreach(var rule in rules.ComputeRiskRules(healthcheckData)) + { + HealthcheckRiskRule risk = new HealthcheckRiskRule(); + risk.Points = rule.Points; + risk.Category = rule.Category; + risk.Model = rule.Model; + risk.RiskId = rule.RiskId; + risk.Rationale = rule.Rationale; + risk.Details = rule.Details; + healthcheckData.RiskRules.Add(risk); + } DisplayAdvancement("Export completed"); - + return healthcheckData; } public class ReachableDomainInfo : IComparable @@ -285,7 +293,7 @@ private void GenerateGeneralData(ADDomainInfo domainInfo, ADWebService adws) #if DEBUG healthcheckData.EngineVersion += " Beta"; #endif - healthcheckData.Level = HealthcheckDataLevel.Full; + healthcheckData.Level = PingCastleReportDataExportLevel.Full; } private void GenerateUserData(ADDomainInfo domainInfo, ADWebService adws) @@ -306,62 +314,59 @@ private void GenerateUserData(ADDomainInfo domainInfo, ADWebService adws) "whenCreated", }; - healthcheckData.UserAccountData = new HealthcheckAccountData(); - WorkOnReturnedObjectByADWS callback = (ADItem x) => { - // krbtgt - if (x.ObjectSid.IsWellKnown(System.Security.Principal.WellKnownSidType.AccountKrbtgtSid)) - { - healthcheckData.KrbtgtLastChangeDate = x.PwdLastSet; - return; - } - // admin account - if (x.ObjectSid.IsWellKnown(System.Security.Principal.WellKnownSidType.AccountAdministratorSid)) - { - healthcheckData.AdminLastLoginDate = x.LastLogonTimestamp; - healthcheckData.AdminAccountName = x.SAMAccountName; - } - // ignore trust account - if (x.Name.EndsWith("$", StringComparison.InvariantCultureIgnoreCase) && ((x.UserAccountControl & 2048) != 0)) - { - return; - } - ProcessAccountData(healthcheckData.UserAccountData, x, false); - if ((x.UserAccountControl & 0x00000002) == 0) - { - // login script - string scriptName = "None"; - if (!String.IsNullOrEmpty(x.ScriptPath)) - { - scriptName = x.ScriptPath.ToLowerInvariant(); - } - if (!loginscript.ContainsKey(scriptName)) - loginscript[scriptName] = 1; - else - loginscript[scriptName]++; - } + try + { + // krbtgt + if (x.ObjectSid.IsWellKnown(System.Security.Principal.WellKnownSidType.AccountKrbtgtSid)) + { + healthcheckData.KrbtgtLastChangeDate = x.PwdLastSet; + return; + } + // admin account + if (x.ObjectSid.IsWellKnown(System.Security.Principal.WellKnownSidType.AccountAdministratorSid)) + { + healthcheckData.AdminLastLoginDate = x.LastLogonTimestamp; + healthcheckData.AdminAccountName = x.SAMAccountName; + } + // ignore trust account + if (x.Name.EndsWith("$", StringComparison.InvariantCultureIgnoreCase) && ((x.UserAccountControl & 2048) != 0)) + { + return; + } + ProcessAccountData(healthcheckData.UserAccountData, x, false); + if ((x.UserAccountControl & 0x00000002) == 0) + { + // login script + string scriptName = "None"; + if (!String.IsNullOrEmpty(x.ScriptPath)) + { + scriptName = x.ScriptPath.ToLowerInvariant(); + } + if (!loginscript.ContainsKey(scriptName)) + loginscript[scriptName] = 1; + else + loginscript[scriptName]++; + } + } + catch(Exception) + { + Trace.WriteLine("Exception while working on " + x.DistinguishedName); + throw; + } }; - string filter = "(&(objectClass=user)(objectCategory=person))"; - if (NumberOfDepthForSplit == 0) - { - adws.Enumerate(domainInfo.DefaultNamingContext, filter, properties, callback); - } - else - { - DisplayAdvancement("Using OU split technique with depth=" + NumberOfDepthForSplit); - if (Exploration == null) - Exploration = adws.BuildOUExplorationList(domainInfo.DefaultNamingContext, NumberOfDepthForSplit); - int currentOU = 1; - foreach (ADWebService.OUExploration ou in Exploration) - { - DisplayAdvancement(" * Exporting OU=" + ou.OU + "(" + currentOU++ + "/" + Exploration.Count + ") Type:" + ou.Scope); - adws.Enumerate(ou.OU, filter, properties, callback, ou.Scope); - } - } - healthcheckData.LoginScript = new List(); + string filter = "(|(&(objectClass=user)(objectCategory=person))(objectcategory=msDS-GroupManagedServiceAccount)(objectcategory=msDS-ManagedServiceAccount))"; + adws.Enumerate(() => + { + healthcheckData.UserAccountData = new HealthcheckAccountData(); + loginscript.Clear(); + }, + domainInfo.DefaultNamingContext, filter, properties, callback, "SubTree"); + + healthcheckData.LoginScript = new List(); foreach (string key in loginscript.Keys) { var script = new HealthcheckLoginScriptData(key, loginscript[key]); @@ -398,29 +403,10 @@ List CheckScriptPermission(ADDomainInfo domainI continue; var sid = (SecurityIdentifier)rule.IdentityReference; - string account = null; - if (sid.Value == "S-1-1-0") - { - account = "Everyone"; - } - else if (sid.Value == "S-1-5-11") - { - account = "Authenticated Users"; - } - else if (sid.IsWellKnown(WellKnownSidType.AccountDomainGuestsSid) || sid.IsWellKnown(WellKnownSidType.AccountDomainUsersSid) || sid.IsWellKnown(WellKnownSidType.AuthenticatedUserSid)) - { - try - { - account = ((NTAccount)sid.Translate(typeof(NTAccount))).Value; - } - catch (Exception) - { - account = sid.Value; - } - } - if (account == null) + var account = MatchesBadUsersToCheck(sid); + if (!account.HasValue) continue; - output.Add(new HealthcheckScriptDelegationData() { Account = account, Right = rule.FileSystemRights.ToString() }); + output.Add(new HealthcheckScriptDelegationData() { Account = account.Value.Value, Right = rule.FileSystemRights.ToString() }); } } catch(Exception) @@ -499,7 +485,8 @@ void ProcessAccountData(HealthcheckAccountData data, ADItem x, bool computerChec if (!computerCheck) { // not domain users & guest or the guest account - if (x.PrimaryGroupID != 513 && x.PrimaryGroupID != 514 && !x.ObjectSid.IsWellKnown(System.Security.Principal.WellKnownSidType.AccountGuestSid)) + if (x.PrimaryGroupID != 513 && x.PrimaryGroupID != 514 && !x.ObjectSid.IsWellKnown(System.Security.Principal.WellKnownSidType.AccountGuestSid) + && !(x.PrimaryGroupID == 515 && (string.Equals(x.Class,"msDS-GroupManagedServiceAccount", StringComparison.OrdinalIgnoreCase) || string.Equals(x.Class,"msDS-ManagedServiceAccount", StringComparison.OrdinalIgnoreCase)))) { data.NumberBadPrimaryGroup++; if (data.ListBadPrimaryGroup == null) @@ -535,7 +522,7 @@ void ProcessAccountData(HealthcheckAccountData data, ADItem x, bool computerChec data.ListDesEnabled = new List(); data.ListDesEnabled.Add(GetAccountDetail(x)); } - if ((x.UserAccountControl & 0x01000000) != 0) + if ((x.UserAccountControl & 0x80000) != 0) { data.NumberTrustedToAuthenticateForDelegation++; if (data.ListTrustedToAuthenticateForDelegation == null) @@ -580,9 +567,6 @@ private void GenerateComputerData(ADDomainInfo domainInfo, ADWebService adws) Dictionary operatingSystems = new Dictionary(); Dictionary operatingSystemsDC = new Dictionary(); - healthcheckData.ComputerAccountData = new HealthcheckAccountData(); - healthcheckData.DomainControllers = new List(); - healthcheckData.OperatingSystem = new List(); WorkOnReturnedObjectByADWS callback = (ADItem x) => @@ -598,9 +582,11 @@ private void GenerateComputerData(ADDomainInfo domainInfo, ADWebService adws) // process only not disabled computers if ((x.UserAccountControl & 0x00000002) == 0) { - if (x.PrimaryGroupID == 516 || x.PrimaryGroupID == 521) + // we consider DC as a computer in the special OU or having the primary group ID of DC or Enterprise DC + // known problem: if the DC is a member (not primary group) & not located in the DC OU + if (x.DistinguishedName.Contains("OU=Domain Controllers,DC=") || x.PrimaryGroupID == 516 || x.PrimaryGroupID == 521) { - healthcheckData.NumberOfDC++; + healthcheckData.NumberOfDC++; if (!operatingSystemsDC.ContainsKey(os)) { operatingSystemsDC[os] = 1; @@ -609,10 +595,6 @@ private void GenerateComputerData(ADDomainInfo domainInfo, ADWebService adws) { operatingSystemsDC[os]++; } - } - // domain controllers enabled - if (x.DistinguishedName.Contains("OU=Domain Controllers,DC=")) - { HealthcheckDomainController dc = new HealthcheckDomainController(); dc.DCName = x.Name; dc.CreationDate = x.WhenCreated; @@ -626,22 +608,16 @@ private void GenerateComputerData(ADDomainInfo domainInfo, ADWebService adws) }; string filter = "(&(ObjectCategory=computer))"; - if (NumberOfDepthForSplit == 0) - { - adws.Enumerate(domainInfo.DefaultNamingContext, filter, properties, callback); - } - else - { - DisplayAdvancement("Using OU split technique with depth=" + NumberOfDepthForSplit); - if (Exploration == null) - Exploration = adws.BuildOUExplorationList(domainInfo.DefaultNamingContext, NumberOfDepthForSplit); - int currentOU = 1; - foreach (ADWebService.OUExploration ou in Exploration) - { - DisplayAdvancement(" * Exporting OU=" + ou.OU + "(" + currentOU++ + "/" + Exploration.Count + ") Type:" + ou.Scope); - adws.Enumerate(ou.OU, filter, properties, callback, ou.Scope); - } - } + adws.Enumerate(() => + { + healthcheckData.ComputerAccountData = new HealthcheckAccountData(); + healthcheckData.DomainControllers = new List(); + healthcheckData.OperatingSystem = new List(); + operatingSystems.Clear(); + operatingSystemsDC.Clear(); + }, + domainInfo.DefaultNamingContext, filter, properties, callback, "SubTree"); + foreach (string key in operatingSystems.Keys) { operatingSystems[key].NumberOfOccurence = operatingSystems[key].data.NumberActive; @@ -741,6 +717,10 @@ private string GetOperatingSystem(string os) { return "Windows 2016"; } + if (Regex.Match(os, @"windows(.*) 2019", RegexOptions.IgnoreCase).Success) + { + return "Windows 2019"; + } if (Regex.Match(os, @"windows(.*) 7", RegexOptions.IgnoreCase).Success) { return "Windows 7"; @@ -932,6 +912,7 @@ private void EnrichSIDHistoryWithTrustData(List list) if (trustdata.SID == data.DomainSid) { data.FriendlyName = trustdata.TrustPartner; + data.NetBIOSName = trustdata.NetBiosName; break; } if (trustdata.KnownDomains != null) @@ -941,13 +922,17 @@ private void EnrichSIDHistoryWithTrustData(List list) if (trustdomainInfo.Sid == data.DomainSid) { data.FriendlyName = trustdomainInfo.DnsName; + data.NetBIOSName = trustdomainInfo.NetbiosName; break; } } } } - if (data.DomainSid == healthcheckData.DomainSid) - data.FriendlyName = healthcheckData.DomainFQDN; + if (data.DomainSid == healthcheckData.DomainSid) + { + data.FriendlyName = healthcheckData.DomainFQDN; + data.NetBIOSName = healthcheckData.NetBIOSName; + } if (String.IsNullOrEmpty(data.FriendlyName)) data.FriendlyName = data.DomainSid; } @@ -1134,6 +1119,8 @@ private void GeneratePrivilegedGroupData(ADDomainInfo domainInfo, ADWebService a new KeyValuePair(domainInfo.DomainSid + "-519","Enterprise Admins"), new KeyValuePair(domainInfo.DomainSid + "-518","Schema Admins"), new KeyValuePair(domainInfo.DomainSid + "-517","Cert Publishers"), + new KeyValuePair(domainInfo.DomainSid + "-526","Key Admins"), + new KeyValuePair(domainInfo.DomainSid + "-527","Enterprise Key Admins"), }; // adding Dns Admin @@ -1143,14 +1130,22 @@ private void GeneratePrivilegedGroupData(ADDomainInfo domainInfo, ADWebService a "name", "objectSid" }; + bool dnsAdminFound = false; WorkOnReturnedObjectByADWS callback = (ADItem x) => { var temp = new List>(privilegedGroups); temp.Add(new KeyValuePair(x.ObjectSid.Value, "Dns Admins")); privilegedGroups = temp.ToArray(); + dnsAdminFound = true; }; + // we do a one level search just case the group is in the default position adws.Enumerate(domainInfo.DefaultNamingContext, "(&(objectClass=group)(description=DNS Administrators Group))", properties, callback, "OneLevel"); + if (!dnsAdminFound) + { + // then full tree. This is an optimization for LDAP request + adws.Enumerate(domainInfo.DefaultNamingContext, "(&(objectClass=group)(description=DNS Administrators Group))", properties, callback); + } Dictionary allMembers = new Dictionary(); foreach (KeyValuePair privilegedGroup in privilegedGroups) @@ -1210,43 +1205,48 @@ private HealthCheckGroupData AnalyzeGroupData(ADDomainInfo domainInfo, string gr member.Name = x.SAMAccountName; member.PwdLastSet = x.PwdLastSet; member.LastLogonTimestamp = x.LastLogonTimestamp; - if ((x.UserAccountControl & 0x00000002) != 0) - data.NumberOfMemberDisabled++; - else - { - data.NumberOfMemberEnabled++; - member.IsEnabled = true; - // last login since 6 months - if (x.LastLogonTimestamp.AddDays(6 * 31) > DateTime.Now) - { - data.NumberOfMemberActive++; - member.IsActive = true; - } - else - data.NumberOfMemberInactive++; - } - if ((x.UserAccountControl & 0x00000010) != 0) - { - member.IsLocked = true; - data.NumberOfMemberLocked++; - } - if ((x.UserAccountControl & 0x00010000) != 0) - { - data.NumberOfMemberPwdNeverExpires++; - member.DoesPwdNeverExpires = true; - } - if ((x.UserAccountControl & 0x00000020) != 0) - data.NumberOfMemberPwdNotRequired++; - // this account is sensitive and cannot be delegated - if ((x.UserAccountControl & 0x100000) == 0) - { - data.NumberOfMemberCanBeDelegated++; - member.CanBeDelegated = true; - } - if ((x.UserAccountControl & 0x40000) != 0) + if ((x.UserAccountControl & 0x00000002) != 0) + data.NumberOfMemberDisabled++; + else { - data.NumberOfSmartCardRequired++; - member.SmartCardRequired = true; + data.NumberOfMemberEnabled++; + member.IsEnabled = true; + // last login since 6 months + if (x.LastLogonTimestamp.AddDays(6 * 31) > DateTime.Now) + { + data.NumberOfMemberActive++; + member.IsActive = true; + } + else + data.NumberOfMemberInactive++; + if (x.ServicePrincipalName != null && x.ServicePrincipalName.Length > 0) + { + member.IsService = true; + data.NumberOfServiceAccount++; + } + if ((x.UserAccountControl & 0x00000010) != 0) + { + member.IsLocked = true; + data.NumberOfMemberLocked++; + } + if ((x.UserAccountControl & 0x00010000) != 0) + { + data.NumberOfMemberPwdNeverExpires++; + member.DoesPwdNeverExpires = true; + } + if ((x.UserAccountControl & 0x00000020) != 0) + data.NumberOfMemberPwdNotRequired++; + // this account is sensitive and cannot be delegated + if ((x.UserAccountControl & 0x100000) == 0) + { + data.NumberOfMemberCanBeDelegated++; + member.CanBeDelegated = true; + } + if ((x.UserAccountControl & 0x40000) != 0) + { + data.NumberOfSmartCardRequired++; + member.SmartCardRequired = true; + } } } } @@ -1270,7 +1270,8 @@ private bool GetGroupMembers(ADDomainInfo domainInfo, ADWebService adws, string "lastLogonTimestamp", "pwdLastSet", "sAMAccountName", - "objectClass" + "objectClass", + "servicePrincipalName", }; bool IsObjectFound = false; List FutureDataToSearch = new List(); @@ -1387,6 +1388,7 @@ private void InspectAdminSDHolder(ADDomainInfo domainInfo, ADWebService adws) data.DistinguishedName = "AdminSDHolder"; data.Account = "Authenticated Users"; data.Right = "Not allowed to read AdminSDHolder"; + data.SecurityIdentifier = string.Empty; } } @@ -1407,7 +1409,8 @@ private void AddAdminSDHolderSDDLRulesToDelegation(List rulesAdded, ADDo { HealthcheckDelegationData data = new HealthcheckDelegationData(); data.DistinguishedName = "AdminSDHolder"; - data.Account = NativeMethods.ConvertSIDToName(key, domainInfo.DnsHostName); + data.SecurityIdentifier = key; + data.Account = NativeMethods.ConvertSIDToName(key, domainInfo.DnsHostName); data.Right = dic[key]; healthcheckData.Delegations.Add(data); } @@ -1436,60 +1439,73 @@ private List CompareSecurityDescriptor(string sddlToCheck, string sddlRe } - private void InspectDelegation(ADDomainInfo domainInfo, ADWebService adws) - { - string[] properties = new string[] { - "distinguishedName", - "name", - "nTSecurityDescriptor", - }; - Dictionary sidCache = new Dictionary(); - WorkOnReturnedObjectByADWS callback = - (ADItem x) => - { - Dictionary permissions = new Dictionary(); - FilterDelegation(x, - (SecurityIdentifier sid, string right) - => - { - if (!permissions.ContainsKey(sid.Value)) - { - permissions[sid.Value] = right; - } - else - { - permissions[sid.Value] += ", " + right; - } - } - ); - foreach (string sid in permissions.Keys) - { - HealthcheckDelegationData delegation = new HealthcheckDelegationData(); - healthcheckData.Delegations.Add(delegation); - delegation.DistinguishedName = x.DistinguishedName; - // avoid translation for anomaly detection later - if (sid == "S-1-1-0") - { - delegation.Account = "Everyone"; - } - else if (sid == "S-1-5-11") - { - delegation.Account = "Authenticated Users"; - } - else - { - if (!sidCache.ContainsKey(sid)) - { - sidCache[sid] = NativeMethods.ConvertSIDToName(sid, domainInfo.DnsHostName); - } - delegation.Account = sidCache[sid]; - } - delegation.Right = permissions[sid]; - } + private void InspectDelegation(ADDomainInfo domainInfo, ADWebService adws) + { + string[] properties = new string[] { + "distinguishedName", + "name", + "nTSecurityDescriptor", + }; + Dictionary sidCache = new Dictionary(); + WorkOnReturnedObjectByADWS callback = + (ADItem x) => + { + Dictionary permissions = new Dictionary(); + FilterDelegation(x, + (SecurityIdentifier sid, string right) + => + { + if (!permissions.ContainsKey(sid.Value)) + { + permissions[sid.Value] = right; + } + else + { + permissions[sid.Value] += ", " + right; + } + } + ); + foreach (string sid in permissions.Keys) + { + HealthcheckDelegationData delegation = new HealthcheckDelegationData(); + healthcheckData.Delegations.Add(delegation); + delegation.DistinguishedName = x.DistinguishedName; + delegation.SecurityIdentifier = sid; + // avoid translation for anomaly detection later + if (sid == "S-1-1-0") + { + delegation.Account = "Everyone"; + } + else if (sid == "S-1-5-11") + { + delegation.Account = "Authenticated Users"; + } + else if (sid.EndsWith("-513")) + { + delegation.Account = "Domain Users"; + } + else if (sid.EndsWith("-515")) + { + delegation.Account = "Domain Computers"; + } + else + { + if (!sidCache.ContainsKey(sid)) + { + sidCache[sid] = NativeMethods.ConvertSIDToName(sid, domainInfo.DnsHostName); + } + delegation.Account = sidCache[sid]; + } + delegation.Right = permissions[sid]; + } - }; + }; - adws.Enumerate(domainInfo.DefaultNamingContext, "(|(objectCategory=organizationalUnit)(objectCategory=container)(objectCategory=domain)(objectCategory=buitinDomain))", properties, callback); + adws.Enumerate( + () => { + healthcheckData.Delegations.Clear(); + }, + domainInfo.DefaultNamingContext, "(|(objectCategory=organizationalUnit)(objectCategory=container)(objectCategory=domain)(objectCategory=buitinDomain))", properties, callback, "SubTree"); } // removed unexpire password because permissions given to authenticated users at the root of the domain @@ -1699,6 +1715,7 @@ private void GenerateGPOData(ADDomainInfo domainInfo, ADWebService adws, Network healthcheckData.GPOLoginScript = new List(); healthcheckData.GPOLocalMembership = new List(); healthcheckData.GPOEventForwarding = new List(); + healthcheckData.GPODelegation = new List(); // subitility: GPOList = all active and not active GPO (but not the deleted ones) Dictionary GPOList = new Dictionary(); @@ -1844,10 +1861,12 @@ private void GenerateNTLMStoreData(ADDomainInfo domainInfo, ADWebService adws) void ThreadGPOAnalysis(DirectoryInfo directoryInfo, string GPOName, ADDomainInfo domainInfo, bool IsGPOActive) { + string step = "initial"; try { string path; // work on all GPO including disabled ones + step = "extract GPP passwords"; foreach(string target in new string[] {"user", "machine"}) { foreach (string shortname in new string[] { @@ -1868,11 +1887,14 @@ void ThreadGPOAnalysis(DirectoryInfo directoryInfo, string GPOName, ADDomainInfo path = directoryInfo.FullName + @"\Machine\Microsoft\Windows nt\SecEdit\GptTmpl.inf"; if (File.Exists(path)) { + step = "extract GPP privileges"; ExtractGPPPrivilegePasswordLsaSettingEtc(path, GPOName, domainInfo); } + step = "extract GPO login script"; ExtractGPOLoginScript(domainInfo, directoryInfo, GPOName); try { + step = "extract Registry Pol info"; ExtractRegistryPolInfo(directoryInfo, GPOName); } catch (Exception ex) @@ -1881,6 +1903,8 @@ void ThreadGPOAnalysis(DirectoryInfo directoryInfo, string GPOName, ADDomainInfo Trace.WriteLine("Exception " + ex.Message); Trace.WriteLine(ex.StackTrace); } + step = "check GPO permissions"; + ExtractGPODelegation(directoryInfo.FullName, GPOName); } catch (UnauthorizedAccessException ex) @@ -1899,8 +1923,8 @@ void ThreadGPOAnalysis(DirectoryInfo directoryInfo, string GPOName, ADDomainInfo { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("Unable to analyze the GPO: " + directoryInfo.Name + "(" + ex.Message + ")"); - Trace.WriteLine("Unable to analyze the GPO: " + directoryInfo.Name + "(" + ex.Message + ")"); - Console.WriteLine(ex.StackTrace); + Trace.WriteLine("Unable to analyze the GPO: " + directoryInfo.Name + "(" + ex.Message + ")"); + Console.WriteLine("More details are available in the trace log (step: " + step + ")"); Trace.WriteLine(ex.StackTrace); Console.ResetColor(); } @@ -2026,6 +2050,101 @@ private void ExtractRegistryPolInfo(DirectoryInfo directoryInfo, string GPOName) } } + private KeyValuePair? MatchesBadUsersToCheck(SecurityIdentifier sid) + { + if (sid.Value == "S-1-1-0") + { + return new KeyValuePair(sid, "Everyone"); + } + else if (sid.Value == "S-1-5-11") + { + return new KeyValuePair(sid, "Authenticated Users"); + } + else if (sid.IsWellKnown(WellKnownSidType.AccountDomainGuestsSid) || sid.IsWellKnown(WellKnownSidType.AccountDomainUsersSid) || sid.IsWellKnown(WellKnownSidType.AuthenticatedUserSid)) + { + try + { + return new KeyValuePair(sid, ((NTAccount)sid.Translate(typeof(NTAccount))).Value); + } + catch (Exception) + { + return new KeyValuePair(sid, sid.Value); + } + } + return null; + } + + private void ExtractGPODelegation(string path, string GPOName) + { + if (!Directory.Exists(path)) + return; + + if (!Directory.Exists(path)) + return; + var dirs = new List(Directory.GetDirectories(path, "*", SearchOption.AllDirectories)); + dirs.Insert(0, path); + foreach (var dirname in dirs) + { + try + { + ExtractGPODelegationAnalyzeAccessControl(GPOName, Directory.GetAccessControl(dirname), dirname, (path == dirname)); + } + catch (Exception) + { + } + } + foreach (var filename in Directory.GetFiles(path, "*.*", SearchOption.AllDirectories)) + { + try + { + ExtractGPODelegationAnalyzeAccessControl(GPOName, File.GetAccessControl(filename), filename, false); + } + catch (Exception) + { + } + } + } + + void ExtractGPODelegationAnalyzeAccessControl(string GPOName, FileSystemSecurity security, string name, bool includeInherited) + { + var Owner = (SecurityIdentifier)security.GetOwner(typeof(SecurityIdentifier)); + var matchOwner = MatchesBadUsersToCheck(Owner); + if (matchOwner.HasValue) + { + healthcheckData.GPODelegation.Add(new GPODelegationData() + { + GPOName = GPOName, + Item = name, + Right = "Owner", + Account = matchOwner.Value.Value, + } + ); + } + var accessRules = security.GetAccessRules(true, includeInherited, typeof(SecurityIdentifier)); + if (accessRules == null) + return; + + foreach (FileSystemAccessRule accessrule in accessRules) + { + if (accessrule.AccessControlType == AccessControlType.Deny) + continue; + if ((FileSystemRights.Write & accessrule.FileSystemRights) != FileSystemRights.Write) + continue; + + var match = MatchesBadUsersToCheck((SecurityIdentifier) accessrule.IdentityReference); + if (!match.HasValue) + continue; + healthcheckData.GPODelegation.Add(new GPODelegationData() + { + GPOName = GPOName, + Item = name, + Right = accessrule.FileSystemRights.ToString(), + Account = match.Value.Value, + } + ); + } + } + private void GetGPOList(ADDomainInfo domainInfo, ADWebService adws, Dictionary GPOList, List GPOListDisabled) { @@ -2160,79 +2279,87 @@ private void ParseGPOLoginScript(ADDomainInfo domainInfo, string path, string GP } - private void ExtractGPPPassword(string shortname, string fullname, string GPOName) - { - string xpath = null; - switch (shortname) - { - case "groups.xml": - xpath = "/Groups/User"; - break; - case "services.xml": - xpath = "/NTServices/NTService"; - break; - case "scheduledtasks.xml": - xpath = "/ScheduledTasks/Task"; - break; - case "datasources.xml": - xpath = "/DataSources/DataSource"; - break; - case "printers.xml": - xpath = "/Printers/SharedPrinter"; - break; - case "drives.xml": - xpath = "/Drives/Drive"; - break; - default: - return; - } - - XmlDocument doc = new XmlDocument(); - doc.Load(fullname); - XmlNodeList nodeList = doc.SelectNodes(xpath); - foreach (XmlNode node in nodeList) - { - XmlNode password = node.SelectSingleNode("Properties/@cpassword"); - // no password - if (password == null) - continue; - // password has been manually changed - if (String.IsNullOrEmpty(password.Value)) - continue; - GPPPassword PasswordData = new GPPPassword(); - PasswordData.GPOName = GPOName; - PasswordData.Password = DecodeGPPPassword(password.Value); - - XmlNode accountNameNode = node.SelectSingleNode("Properties/@accountName"); - XmlNode userNameNode = node.SelectSingleNode("Properties/@userName"); - PasswordData.UserName = (accountNameNode != null ? accountNameNode.Value : userNameNode.Value); - - XmlNode changed = node.SelectSingleNode("@changed"); - if (changed != null) - PasswordData.Changed = DateTime.Parse(node.SelectSingleNode("@changed").Value); - else - { - FileInfo fi = new FileInfo(fullname); - PasswordData.Changed = fi.LastWriteTime; - } + private void ExtractGPPPassword(string shortname, string fullname, string GPOName) + { + string xpath = null; + string xpathUser = "Properties/@userName"; + string xpathNewName = null; + switch (shortname) + { + case "groups.xml": + xpath = "/Groups/User"; + xpathNewName = "Properties/@newName"; + break; + case "services.xml": + xpath = "/NTServices/NTService"; + xpathUser = "Properties/@accountName"; + break; + case "scheduledtasks.xml": + xpath = "/ScheduledTasks/Task"; + xpathUser = "Properties/@runAs"; + break; + case "datasources.xml": + xpath = "/DataSources/DataSource"; + break; + case "printers.xml": + xpath = "/Printers/SharedPrinter"; + break; + case "drives.xml": + xpath = "/Drives/Drive"; + break; + default: + return; + } - XmlNode newNameNode = node.SelectSingleNode("Properties/@newName"); - if (newNameNode != null && !String.IsNullOrEmpty(newNameNode.Value)) - { - PasswordData.Other = "NewName:" + newNameNode.Value; - } - XmlNode pathNode = node.SelectSingleNode("Properties/@path"); - if (pathNode != null && !String.IsNullOrEmpty(pathNode.Value)) - { - PasswordData.Other = "Path:" + pathNode.Value; - } - PasswordData.Type = shortname; - lock (healthcheckData.GPPPassword) - { - healthcheckData.GPPPassword.Add(PasswordData); - } - } - } + XmlDocument doc = new XmlDocument(); + doc.Load(fullname); + XmlNodeList nodeList = doc.SelectNodes(xpath); + foreach (XmlNode node in nodeList) + { + XmlNode password = node.SelectSingleNode("Properties/@cpassword"); + // no password + if (password == null) + continue; + // password has been manually changed + if (String.IsNullOrEmpty(password.Value)) + continue; + GPPPassword PasswordData = new GPPPassword(); + PasswordData.GPOName = GPOName; + PasswordData.Password = DecodeGPPPassword(password.Value); + + XmlNode userNameNode = node.SelectSingleNode(xpathUser); + PasswordData.UserName = (userNameNode != null ? userNameNode.Value : string.Empty); + + XmlNode changed = node.SelectSingleNode("@changed"); + if (changed != null) + { + PasswordData.Changed = DateTime.Parse(changed.Value); + } + else + { + FileInfo fi = new FileInfo(fullname); + PasswordData.Changed = fi.LastWriteTime; + } + if (xpathNewName != null) + { + XmlNode newNameNode = node.SelectSingleNode(xpathNewName); + if (newNameNode != null && !String.IsNullOrEmpty(newNameNode.Value)) + { + PasswordData.Other = "NewName:" + newNameNode.Value; + } + } + XmlNode pathNode = node.SelectSingleNode("Properties/@path"); + if (pathNode != null && !String.IsNullOrEmpty(pathNode.Value)) + { + PasswordData.Other = "Path:" + pathNode.Value; + } + PasswordData.Type = shortname; + lock (healthcheckData.GPPPassword) + { + healthcheckData.GPPPassword.Add(PasswordData); + } + } + } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Ne pas supprimer d'objets plusieurs fois")] private string DecodeGPPPassword(string encryptedPassword) @@ -2540,16 +2667,20 @@ private void SubExtractLsaSettingsBis(string line, string GPOName) private void SubExtractPrivilege(string line, string GPOName, ADDomainInfo domainInfo) { - string[] privileges = new string[] { - "SeBackupPrivilege", - "SeCreateTokenPrivilege", - "SeDebugPrivilege", - "SeEnableDelegationPrivilege", - "SeSyncAgentPrivilege", - "SeTakeOwnershipPrivilege", - "SeTcbPrivilege", - "SeTrustedCredManAccessPrivilege", - "SeMachineAccountPrivilege", + string[] privileges = new string[] { + "SeBackupPrivilege", + "SeCreateTokenPrivilege", + "SeDebugPrivilege", + "SeEnableDelegationPrivilege", + "SeSyncAgentPrivilege", + "SeTakeOwnershipPrivilege", + "SeTcbPrivilege", + "SeTrustedCredManAccessPrivilege", + "SeMachineAccountPrivilege", + "SeLoadDriverPrivilege", + "SeRestorePrivilege", + "SeImpersonatePrivilege", + "SeAssignPrimaryTokenPrivilege", }; foreach (string privilege in privileges) { @@ -2576,6 +2707,21 @@ private void SubExtractPrivilege(string line, string GPOName, ADDomainInfo domai { continue; } + // SERVICE + if (user.StartsWith("*S-1-5-6", StringComparison.InvariantCultureIgnoreCase)) + { + continue; + } + // LOCAL_SERVICE + if (user.StartsWith("*S-1-5-19", StringComparison.InvariantCultureIgnoreCase)) + { + continue; + } + // NETWORK_SERVICE + if (user.StartsWith("*S-1-5-20", StringComparison.InvariantCultureIgnoreCase)) + { + continue; + } GPPRightAssignment right = new GPPRightAssignment(); lock (healthcheckData.GPPRightAssignment) { @@ -2593,7 +2739,18 @@ private void SubExtractPrivilege(string line, string GPOName, ADDomainInfo domai } else if (user.StartsWith("*S-1", StringComparison.InvariantCultureIgnoreCase)) { - right.User = NativeMethods.ConvertSIDToName(user.Substring(1), domainInfo.DnsHostName); + if (user.EndsWith("-513")) + { + right.User = "Domain Users"; + } + else if (user.EndsWith("-515")) + { + right.User = "Domain Computers"; + } + else + { + right.User = NativeMethods.ConvertSIDToName(user.Substring(1), domainInfo.DnsHostName); + } } else { @@ -2682,10 +2839,12 @@ private void GenerateAnomalies(ADDomainInfo domainInfo, ADWebService adws) healthcheckData.LAPSInstalled = DateTime.MaxValue; string[] propertiesLaps = new string[] { "whenCreated" }; - adws.Enumerate(domainInfo.SchemaNamingContext, "(name=ms-MCS-AdmPwd)", propertiesLaps, (ADItem aditem) => { healthcheckData.LAPSInstalled = aditem.WhenCreated; }, "OneLevel"); + // note: the LDAP request does not contain ms-MCS-AdmPwd because in the old time, MS consultant was installing customized version of the attriute, * being replaced by the company name + // check the oid instead ? (which was the same even if the attribute name was not) + adws.Enumerate(domainInfo.SchemaNamingContext, "(name=ms-*-AdmPwd)", propertiesLaps, (ADItem aditem) => { healthcheckData.LAPSInstalled = aditem.WhenCreated; }, "OneLevel"); // adding the domain sid - string[] properties = new string[] { + string[] propertiesAdminCount = new string[] { "distinguishedName", "name", "sAMAccountName", @@ -2710,7 +2869,7 @@ private void GenerateAnomalies(ADDomainInfo domainInfo, ADWebService adws) } }; - adws.Enumerate(domainInfo.DefaultNamingContext, "(&(objectClass=user)(objectCategory=person)(admincount=1)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(sAMAccountName=krbtgt)))", properties, callbackAdminSDHolder); + adws.Enumerate(domainInfo.DefaultNamingContext, "(&(objectClass=user)(objectCategory=person)(admincount=1)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(sAMAccountName=krbtgt)))", propertiesAdminCount, callbackAdminSDHolder); healthcheckData.AdminSDHolderNotOKCount = healthcheckData.AdminSDHolderNotOK.Count; string[] smartCardNotOKProperties = new string[] { @@ -2847,10 +3006,24 @@ private void GenerateDomainControllerData(ADDomainInfo domainInfo) } foreach( var address in addresses) { - DC.IP.Add(address.ToString()); + string addressString = address.ToString(); + switch(addressString) + { + // avoid registering the loopback address + case "::1": + case "127.0.0.1": + break; + default: + DC.IP.Add(addressString); + break; + } } DC.StartupTime = NativeMethods.GetStartupTime(dns); - if (!SkipNullSession && DC.StartupTime != DateTime.MinValue) + if (DC.StartupTime == DateTime.MinValue) + { + // startup time could not be obtained - consider the DC as down + } + if (!SkipNullSession) { NullSessionTester session = new NullSessionTester(dns); if (session.EnumerateAccount(1)) @@ -2869,6 +3042,7 @@ private void GenerateDomainControllerData(ADDomainInfo domainInfo) DC.SupportSMB2OrSMB3 = true; } DC.SMB2SecurityMode = securityMode; + DC.RemoteSpoolerDetected = SpoolerScanner.CheckIfTheSpoolerIsActive(dns); } }; diff --git a/Healthcheck/HealthcheckRules.cs b/Healthcheck/HealthcheckRules.cs deleted file mode 100644 index 1bddc3b..0000000 --- a/Healthcheck/HealthcheckRules.cs +++ /dev/null @@ -1,203 +0,0 @@ -// -// Copyright (c) Ping Castle. All rights reserved. -// https://www.pingcastle.com -// -// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. -// -using PingCastle.Healthcheck.Rules; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Reflection; -using System.Security.Cryptography.X509Certificates; -using System.Text; - -namespace PingCastle.Healthcheck -{ - - public class HealthcheckRules - { - private static List _rules = null; - - public static List Rules { - get - { - if (_rules == null) - { - _rules = LoadRules(); - } - return _rules; - } - } - private static List LoadRules() - { - // important: to work with W2000, we cannot use GetType because it will instanciate .Net 3.0 class then load the missing assembly - // the trick here is to check only the exported type and put as internal the class using .Net 3.0 functionalities - var output = new List(); - foreach (Type type in Assembly.GetAssembly(typeof(HealthcheckRules)).GetExportedTypes()) - { - if (type.IsSubclassOf(typeof(HeatlcheckRuleBase))) - { - output.Add((HeatlcheckRuleBase)Activator.CreateInstance(type)); - } - } - return output; - } - - // when multiple reports are ran each after each other, internal state can be kept - void ReInitRule(HeatlcheckRuleBase rule) - { - rule.Initialize(); - } - - public void ComputeRiskRules(HealthcheckData healthcheckData) - { - healthcheckData.RiskRules = new List(); - Trace.WriteLine("Begining to run risk rule"); - foreach (HeatlcheckRuleBase rule in Rules) - { - Trace.WriteLine("Rule: " + rule.GetType().ToString()); - ReInitRule(rule); - if (rule.Analyze(healthcheckData)) - { - Trace.WriteLine(" matched"); - HealthcheckRiskRule risk = new HealthcheckRiskRule(); - risk.Points = rule.Points; - risk.Category = rule.Category; - risk.Model = rule.Model; - risk.RiskId = rule.Id; - risk.Rationale = rule.Rationale; - risk.Details = rule.Details; - healthcheckData.RiskRules.Add(risk); - } - } - Trace.WriteLine("Risk rule run stopped"); - ReComputeTotals(healthcheckData); - } - public static void ReComputeTotals(HealthcheckData healthcheckData) - { - // consolidate scores - healthcheckData.GlobalScore = 0; - healthcheckData.StaleObjectsScore = 0; - healthcheckData.PrivilegiedGroupScore = 0; - healthcheckData.TrustScore = 0; - healthcheckData.AnomalyScore = 0; - foreach (HealthcheckRiskRule rule in healthcheckData.RiskRules) - { - switch (rule.Category) - { - case HealthcheckRiskRuleCategory.Anomalies: - healthcheckData.AnomalyScore += rule.Points; - break; - case HealthcheckRiskRuleCategory.PrivilegedAccounts: - healthcheckData.PrivilegiedGroupScore += rule.Points; - break; - case HealthcheckRiskRuleCategory.StaleObjects: - healthcheckData.StaleObjectsScore += rule.Points; - break; - case HealthcheckRiskRuleCategory.Trusts: - healthcheckData.TrustScore += rule.Points; - break; - } - } - // limit to 100 - if (healthcheckData.StaleObjectsScore > 100) - healthcheckData.StaleObjectsScore = 100; - if (healthcheckData.PrivilegiedGroupScore > 100) - healthcheckData.PrivilegiedGroupScore = 100; - if (healthcheckData.TrustScore > 100) - healthcheckData.TrustScore = 100; - if (healthcheckData.AnomalyScore > 100) - healthcheckData.AnomalyScore = 100; - // max of all scores - healthcheckData.GlobalScore = Math.Max(healthcheckData.StaleObjectsScore, - Math.Max(healthcheckData.PrivilegiedGroupScore, - Math.Max(healthcheckData.TrustScore, healthcheckData.AnomalyScore))); - } - - /* - public void GenerateRuleDescriptionFile(string filename) - { - Microsoft.Office.Interop.Excel.Application xlApp = new Microsoft.Office.Interop.Excel.Application(); - if (xlApp == null) - { - Trace.WriteLine("Excel not installed"); - string output = null; - output += "\"Id\"\t\"Category\"\t\"Points\"\t\"Description\"\t\"Solution\"\r\n"; - foreach (HeatlcheckRuleBase rule in Rules) - { - output += "\"" + rule.Id + "\"\t\"" + rule.Category.ToString() + "\"\t" + rule.Points + "\t\"" + rule.Description.Replace("\"", "\"\"") + "\"\t\"" + rule.Solution.Replace("\"", "\"\"") + "\"\r\n"; - } - File.WriteAllText(filename, output); - throw new InvalidOperationException("Excel cannot be found - using txt output instead"); - } - try - { - // excel save by default to documents - string fullPath = new FileInfo(filename).FullName; - xlApp.Visible = false; - - Workbook wb = xlApp.Workbooks.Add(XlWBATemplate.xlWBATWorksheet); - Worksheet ws = (Worksheet)wb.Worksheets[1]; - - ws.Name = "hc-rule"; - ws.Cells[1, 1] = "ID"; - ws.Cells[1, 2] = "Category"; - ws.Cells[1, 3] = "Points"; - ws.Cells[1, 4] = "Description"; - ws.Cells[1, 5] = "Solution"; - int i = 2; - foreach (HeatlcheckRuleBase rule in Rules) - { - ws.Cells[i, 1] = rule.Id; - ws.Cells[i, 2] = rule.Category.ToString(); - ws.Cells[i, 3] = rule.Points; - ws.Cells[i, 4] = rule.Description; - ws.Cells[i, 5] = rule.Solution; - i++; - ws.Columns.AutoFit(); - } - ws.Rows.AutoFit(); - Range tRange = ws.get_Range(ws.Cells[1, 1], ws.Cells[i - 1, 5]); - ws.ListObjects.Add(XlListObjectSourceType.xlSrcRange, tRange, - Type.Missing, XlYesNoGuess.xlYes, Type.Missing).Name = "RuleTable"; - ws.ListObjects["RuleTable"].TableStyle = "TableStyleMedium3"; - xlApp.DisplayAlerts = false; - wb.SaveAs(Filename: fullPath); - xlApp.DisplayAlerts = true; - xlApp.Quit(); - } - catch (Exception) - { - xlApp.Visible = true; - throw; - } - } - */ - - public static string GetRuleDescription(string ruleid) - { - foreach (var rule in Rules) - { - if (rule.Id == ruleid) - { - return rule.Title; - } - } - return String.Empty; - } - - public static HeatlcheckRuleBase GetRuleFromID(string ruleid) - { - foreach (var rule in Rules) - { - if (rule.Id == ruleid) - { - return rule; - } - } - return null; - } - } -} diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyAdminSDHolder.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyAdminSDHolder.cs index 17f73b4..2b29b53 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyAdminSDHolder.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyAdminSDHolder.cs @@ -7,19 +7,21 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-AdminSDHolder", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.TemporaryAdmins)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 50, Threshold: 50, Order: 1)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 45, Threshold: 45, Order: 2)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 40, Threshold: 40, Order: 3)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 35, Threshold: 35, Order: 4)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 30, Threshold: 30, Order: 5)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 25, Threshold: 25, Order: 6)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 20, Threshold: 20, Order: 7)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 15, Order: 8)] - public class HeatlcheckRuleAnomalyAdminSDHolder : HeatlcheckRuleBase + [RuleModel("A-AdminSDHolder", RiskRuleCategory.Anomalies, RiskModelCategory.TemporaryAdmins)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 50, Threshold: 50, Order: 1)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 45, Threshold: 45, Order: 2)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 40, Threshold: 40, Order: 3)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 35, Threshold: 35, Order: 4)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 30, Threshold: 30, Order: 5)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 25, Threshold: 25, Order: 6)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 20, Threshold: 20, Order: 7)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 15, Order: 8)] + [RuleANSSI("R40", "paragraph.3.6.3.1")] + public class HeatlcheckRuleAnomalyAdminSDHolder : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyAnonymousAuthorizedGPO.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyAnonymousAuthorizedGPO.cs index b63d0cd..bff7336 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyAnonymousAuthorizedGPO.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyAnonymousAuthorizedGPO.cs @@ -7,12 +7,13 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-AnonymousAuthorizedGPO", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.Reconnaissance)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 5)] - public class HeatlcheckRuleAnomalyAnonymousAuthorizedGPO : HeatlcheckRuleBase + [RuleModel("A-AnonymousAuthorizedGPO", RiskRuleCategory.Anomalies, RiskModelCategory.Reconnaissance)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] + public class HeatlcheckRuleAnomalyAnonymousAuthorizedGPO : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyBackupMetadata.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyBackupMetadata.cs index c18a49d..daeea91 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyBackupMetadata.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyBackupMetadata.cs @@ -7,13 +7,14 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-BackupMetadata", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.Backup)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 15, Threshold: 7)] - [HeatlcheckRuleSTIG("V-25385")] - public class HeatlcheckRuleAnomalyBackupMetadata : HeatlcheckRuleBase + [RuleModel("A-BackupMetadata", RiskRuleCategory.Anomalies, RiskModelCategory.Backup)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 15, Threshold: 7)] + [RuleSTIG("V-25385", "Active Directory data must be backed up daily for systems with a Risk Management Framework categorization for Availability of moderate or high. Systems with a categorization of low must be backed up weekly.")] + public class HeatlcheckRuleAnomalyBackupMetadata : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD2Intermediate.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD2Intermediate.cs index c18a0fd..2c02cf0 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD2Intermediate.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD2Intermediate.cs @@ -8,12 +8,13 @@ using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-MD2IntermediateCert", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.CertificateTakeOver)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 10)] - public class HeatlcheckRuleAnomalyCertMD2Intermediate : HeatlcheckRuleBase + [RuleModel("A-MD2IntermediateCert", RiskRuleCategory.Anomalies, RiskModelCategory.CertificateTakeOver)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 10)] + public class HeatlcheckRuleAnomalyCertMD2Intermediate : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD2Root.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD2Root.cs index 55f0e37..11d1076 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD2Root.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD2Root.cs @@ -8,12 +8,13 @@ using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-MD2RootCert", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.CertificateTakeOver)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 0)] - public class HeatlcheckRuleAnomalyCertMD2Root : HeatlcheckRuleBase + [RuleModel("A-MD2RootCert", RiskRuleCategory.Anomalies, RiskModelCategory.CertificateTakeOver)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 0)] + public class HeatlcheckRuleAnomalyCertMD2Root : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD4Intermediate.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD4Intermediate.cs index 4c7a61c..8e0e7e8 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD4Intermediate.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD4Intermediate.cs @@ -8,12 +8,13 @@ using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-MD4IntermediateCert", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.CertificateTakeOver)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 10)] - public class HeatlcheckRuleAnomalyCertMD4Intermediate : HeatlcheckRuleBase + [RuleModel("A-MD4IntermediateCert", RiskRuleCategory.Anomalies, RiskModelCategory.CertificateTakeOver)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 10)] + public class HeatlcheckRuleAnomalyCertMD4Intermediate : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD4Root.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD4Root.cs index 2b3177b..06b5c20 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD4Root.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD4Root.cs @@ -8,12 +8,13 @@ using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-MD4RootCert", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.CertificateTakeOver)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 0)] - public class HeatlcheckRuleAnomalyCertMD4Root : HeatlcheckRuleBase + [RuleModel("A-MD4RootCert", RiskRuleCategory.Anomalies, RiskModelCategory.CertificateTakeOver)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 0)] + public class HeatlcheckRuleAnomalyCertMD4Root : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD5Intermediate.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD5Intermediate.cs index 6321399..5efc3c1 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD5Intermediate.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD5Intermediate.cs @@ -8,12 +8,13 @@ using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-MD5IntermediateCert", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.CertificateTakeOver)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 5)] - public class HeatlcheckRuleAnomalyCertMD5Intermediate : HeatlcheckRuleBase + [RuleModel("A-MD5IntermediateCert", RiskRuleCategory.Anomalies, RiskModelCategory.CertificateTakeOver)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] + public class HeatlcheckRuleAnomalyCertMD5Intermediate : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD5Root.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD5Root.cs index 57ef146..359cd56 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD5Root.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertMD5Root.cs @@ -8,12 +8,13 @@ using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-MD5RootCert", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.CertificateTakeOver)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 0)] - public class HeatlcheckRuleAnomalyCertMD5Root : HeatlcheckRuleBase + [RuleModel("A-MD5RootCert", RiskRuleCategory.Anomalies, RiskModelCategory.CertificateTakeOver)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 0)] + public class HeatlcheckRuleAnomalyCertMD5Root : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA0Intermediate.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA0Intermediate.cs index b9fe87c..7110943 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA0Intermediate.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA0Intermediate.cs @@ -8,12 +8,13 @@ using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-SHA0IntermediateCert", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.CertificateTakeOver)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 5)] - public class HeatlcheckRuleAnomalyCertSHA0Intermediate : HeatlcheckRuleBase + [RuleModel("A-SHA0IntermediateCert", RiskRuleCategory.Anomalies, RiskModelCategory.CertificateTakeOver)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] + public class HeatlcheckRuleAnomalyCertSHA0Intermediate : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA0Root.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA0Root.cs index 7de76db..3e89402 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA0Root.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA0Root.cs @@ -8,12 +8,13 @@ using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-SHA0RootCert", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.CertificateTakeOver)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 0)] - public class HeatlcheckRuleAnomalyCertSHA0Root : HeatlcheckRuleBase + [RuleModel("A-SHA0RootCert", RiskRuleCategory.Anomalies, RiskModelCategory.CertificateTakeOver)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 0)] + public class HeatlcheckRuleAnomalyCertSHA0Root : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA1Intermediate.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA1Intermediate.cs index 4f48dfb..619abf1 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA1Intermediate.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA1Intermediate.cs @@ -8,12 +8,13 @@ using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-SHA1IntermediateCert", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.CertificateTakeOver)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 1)] - public class HeatlcheckRuleAnomalyCertSHA1Intermediate : HeatlcheckRuleBase + [RuleModel("A-SHA1IntermediateCert", RiskRuleCategory.Anomalies, RiskModelCategory.CertificateTakeOver)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 1)] + public class HeatlcheckRuleAnomalyCertSHA1Intermediate : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA1Root.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA1Root.cs index dca2ea1..00a326d 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA1Root.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertSHA1Root.cs @@ -8,12 +8,13 @@ using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-SHA1RootCert", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.CertificateTakeOver)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 0)] - public class HeatlcheckRuleAnomalyCertSHA1Root : HeatlcheckRuleBase + [RuleModel("A-SHA1RootCert", RiskRuleCategory.Anomalies, RiskModelCategory.CertificateTakeOver)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 0)] + public class HeatlcheckRuleAnomalyCertSHA1Root : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertWeakRSA.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertWeakRSA.cs index 7a82f66..73ccd3a 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyCertWeakRSA.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyCertWeakRSA.cs @@ -10,12 +10,13 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-WeakRSARootCert", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.CertificateTakeOver)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 5)] - public class HeatlcheckRuleAnomalyCertWeakRSA : HeatlcheckRuleBase + [RuleModel("A-WeakRSARootCert", RiskRuleCategory.Anomalies, RiskModelCategory.CertificateTakeOver)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] + public class HeatlcheckRuleAnomalyCertWeakRSA : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyDCSpooler.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyDCSpooler.cs new file mode 100644 index 0000000..1302ea2 --- /dev/null +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyDCSpooler.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Text; +using PingCastle.Rules; + +namespace PingCastle.Healthcheck.Rules +{ + [RuleModel("A-DC-Spooler", RiskRuleCategory.Anomalies, RiskModelCategory.PassTheCredential)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 10)] + public class HeatlcheckRuleAnomalyDCSpooler : RuleBase + { + protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) + { + foreach (var DC in healthcheckData.DomainControllers) + { + if (DC.RemoteSpoolerDetected) + { + AddRawDetail(DC.DCName); + } + } + return null; + } + } +} diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyDsHeuristicsAnonymous.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyDsHeuristicsAnonymous.cs index 6f6d375..0ff1bca 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyDsHeuristicsAnonymous.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyDsHeuristicsAnonymous.cs @@ -7,13 +7,14 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-DsHeuristicsAnonymous", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.Reconnaissance)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 5)] - [HeatlcheckRuleSTIG("V-8555", true)] - public class HeatlcheckRuleAnomalyDsHeuristicsAnonymous : HeatlcheckRuleBase + [RuleModel("A-DsHeuristicsAnonymous", RiskRuleCategory.Anomalies, RiskModelCategory.Reconnaissance)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] + [RuleSTIG("V-8555", "Anonymous Access to AD forest data above the rootDSE level must be disabled. ", true)] + public class HeatlcheckRuleAnomalyDsHeuristicsAnonymous : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyKrbtgt.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyKrbtgt.cs index eb5d6ef..f8e11d3 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyKrbtgt.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyKrbtgt.cs @@ -7,15 +7,17 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-Krbtgt", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.GoldenTicket)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 50, Threshold: 732, Order: 1)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 40, Threshold: 366, Order: 2)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 30, Threshold: 180, Order: 3)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 20, Threshold: 70, Order: 4)] - public class HeatlcheckRuleAnomalyKrbtgt : HeatlcheckRuleBase + [RuleModel("A-Krbtgt", RiskRuleCategory.Anomalies, RiskModelCategory.GoldenTicket)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 50, Threshold: 732, Order: 1)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 40, Threshold: 366, Order: 2)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 30, Threshold: 180, Order: 3)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 20, Threshold: 70, Order: 4)] + [RuleCERTFR("CERTFR-2014-ACT-032", "SECTION00030000000000000000")] + public class HeatlcheckRuleAnomalyKrbtgt : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyLAPS.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyLAPS.cs index caa1bf9..833070a 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyLAPS.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyLAPS.cs @@ -7,13 +7,15 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-LAPS-Not-Installed", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.PassTheCredential)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 15)] - [HeatlcheckRuleSTIG("V-36438")] - public class HeatlcheckRuleAnomalyLAPS : HeatlcheckRuleBase + [RuleModel("A-LAPS-Not-Installed", RiskRuleCategory.Anomalies, RiskModelCategory.PassTheCredential)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 15)] + [RuleSTIG("V-36438", "Local administrator accounts on domain systems must not share the same password.")] + [RuleCERTFR("CERTFR-2015-ACT-046", "SECTION00020000000000000000")] + public class HeatlcheckRuleAnomalyLAPS : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyLMHash.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyLMHash.cs index 224d717..399070e 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyLMHash.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyLMHash.cs @@ -7,12 +7,15 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-LMHashAuthorized", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.NetworkSniffing)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 5)] - public class HeatlcheckRuleAnomalyLMHash : HeatlcheckRuleBase + [RuleModel("A-LMHashAuthorized", RiskRuleCategory.Anomalies, RiskModelCategory.NetworkSniffing)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] + [RuleANSSI("R37", "paragraph.3.6.2.1")] + [RuleBSI("M 2.412")] + public class HeatlcheckRuleAnomalyLMHash : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyMembershipEveryone.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyMembershipEveryone.cs index 032a1dd..958ff68 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyMembershipEveryone.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyMembershipEveryone.cs @@ -7,12 +7,13 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-MembershipEveryone", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.LocalGroupVulnerability)] - [HeatlcheckRuleComputation(RuleComputationType.PerDiscover, 15)] - public class HeatlcheckRuleAnomalyMembershipEveryone : HeatlcheckRuleBase + [RuleModel("A-MembershipEveryone", RiskRuleCategory.Anomalies, RiskModelCategory.LocalGroupVulnerability)] + [RuleComputation(RuleComputationType.PerDiscover, 15)] + public class HeatlcheckRuleAnomalyMembershipEveryone : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyMinPasswordLen.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyMinPasswordLen.cs index 5764466..9d95006 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyMinPasswordLen.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyMinPasswordLen.cs @@ -7,12 +7,14 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-MinPwdLen", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.WeakPassword)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 10)] - public class HeatlcheckRuleAnomalyMinPasswordLen : HeatlcheckRuleBase + [RuleModel("A-MinPwdLen", RiskRuleCategory.Anomalies, RiskModelCategory.WeakPassword)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 10)] + [RuleBSI("M 4.314")] + public class HeatlcheckRuleAnomalyMinPasswordLen : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyNotEnoughDC.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyNotEnoughDC.cs new file mode 100644 index 0000000..17f84e0 --- /dev/null +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyNotEnoughDC.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Text; +using PingCastle.Rules; + +namespace PingCastle.Healthcheck.Rules +{ + [RuleModel("A-NotEnoughDC", RiskRuleCategory.Anomalies, RiskModelCategory.Backup)] + [RuleComputation(RuleComputationType.TriggerIfLessThan, 5, Threshold: 2)] + public class HeatlcheckRuleAnomalyNotEnoughDC : RuleBase + { + protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) + { + return healthcheckData.NumberOfDC; + } + } +} diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyNullSession.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyNullSession.cs index 02fdbe8..17653f3 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyNullSession.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyNullSession.cs @@ -7,12 +7,14 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-NullSession", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.Reconnaissance)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 10)] - public class HeatlcheckRuleAnomalyNullSession : HeatlcheckRuleBase + [RuleModel("A-NullSession", RiskRuleCategory.Anomalies, RiskModelCategory.Reconnaissance)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 10)] + [RuleBSI("M 2.412")] + public class HeatlcheckRuleAnomalyNullSession : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyPasswordInGPO.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyPasswordInGPO.cs index 556a544..39c951e 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyPasswordInGPO.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyPasswordInGPO.cs @@ -7,12 +7,14 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-PwdGPO", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.PasswordRetrieval)] - [HeatlcheckRuleComputation(RuleComputationType.PerDiscover, 20)] - public class HeatlcheckRuleAnomalyPasswordInGPO : HeatlcheckRuleBase + [RuleModel("A-PwdGPO", RiskRuleCategory.Anomalies, RiskModelCategory.PasswordRetrieval)] + [RuleComputation(RuleComputationType.PerDiscover, 20)] + [RuleCERTFR("CERTFR-2015-ACT-046", "SECTION00020000000000000000")] + public class HeatlcheckRuleAnomalyPasswordInGPO : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyPreWin2000Anonymous.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyPreWin2000Anonymous.cs index 8c9ad71..dd105b2 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyPreWin2000Anonymous.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyPreWin2000Anonymous.cs @@ -7,13 +7,15 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-PreWin2000Anonymous", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.Reconnaissance)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 5)] - [HeatlcheckRuleSTIG("V-8547")] - public class HeatlcheckRuleAnomalyPreWin2000Anonymous : HeatlcheckRuleBase + [RuleModel("A-PreWin2000Anonymous", RiskRuleCategory.Anomalies, RiskModelCategory.Reconnaissance)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] + [RuleSTIG("V-8547", "The Anonymous Logon and Everyone groups must not be members of the Pre-Windows 2000 Compatible Access group.")] + [RuleBSI("M 2.412")] + public class HeatlcheckRuleAnomalyPreWin2000Anonymous : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyRemoteBlankPasswordUse.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyRemoteBlankPasswordUse.cs index 7ce2323..f3ed790 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyRemoteBlankPasswordUse.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyRemoteBlankPasswordUse.cs @@ -7,12 +7,13 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-LimitBlankPasswordUse", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.WeakPassword)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 5)] - public class HeatlcheckRuleAnomalyRemoteBlankPasswordUse : HeatlcheckRuleBase + [RuleModel("A-LimitBlankPasswordUse", RiskRuleCategory.Anomalies, RiskModelCategory.WeakPassword)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] + public class HeatlcheckRuleAnomalyRemoteBlankPasswordUse : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyReversiblePassword.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyReversiblePassword.cs index bef3e13..cc35b4f 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyReversiblePassword.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyReversiblePassword.cs @@ -7,12 +7,13 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-ReversiblePwd", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.PasswordRetrieval)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 10)] - public class HeatlcheckRuleAnomalyReversiblePassword : HeatlcheckRuleBase + [RuleModel("A-ReversiblePwd", RiskRuleCategory.Anomalies, RiskModelCategory.PasswordRetrieval)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 10)] + public class HeatlcheckRuleAnomalyReversiblePassword : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalySMB2SignatureNotEnabled.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalySMB2SignatureNotEnabled.cs index c769fd2..d2ab0ea 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalySMB2SignatureNotEnabled.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalySMB2SignatureNotEnabled.cs @@ -7,12 +7,15 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-SMB2SignatureNotEnabled", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.NetworkSniffing)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 5)] - public class HeatlcheckRuleAnomalySMB2SignatureNotEnabled : HeatlcheckRuleBase + [RuleModel("A-SMB2SignatureNotEnabled", RiskRuleCategory.Anomalies, RiskModelCategory.NetworkSniffing)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] + [RuleBSI("M 2.412")] + [RuleCERTFR("CERTFR-2015-ACT-021", "SECTION00010000000000000000")] + public class HeatlcheckRuleAnomalySMB2SignatureNotEnabled : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalySMB2SignatureNotRequired.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalySMB2SignatureNotRequired.cs index 8137d95..1812f22 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalySMB2SignatureNotRequired.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalySMB2SignatureNotRequired.cs @@ -7,12 +7,15 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-SMB2SignatureNotRequired", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.NetworkSniffing)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 0)] - public class HeatlcheckRuleAnomalySMB2SignatureNotRequired : HeatlcheckRuleBase + [RuleModel("A-SMB2SignatureNotRequired", RiskRuleCategory.Anomalies, RiskModelCategory.NetworkSniffing)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 0)] + [RuleBSI("M 2.412")] + [RuleCERTFR("CERTFR-2015-ACT-021", "SECTION00010000000000000000")] + public class HeatlcheckRuleAnomalySMB2SignatureNotRequired : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalySchemaProtectedUsers.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalySchemaProtectedUsers.cs index 5440785..3bdddc2 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalySchemaProtectedUsers.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalySchemaProtectedUsers.cs @@ -7,13 +7,15 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-ProtectedUsers", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.PassTheCredential)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 0)] - [HeatlcheckRuleSTIG("V-78131")] - public class HeatlcheckRuleAnomalySchemaProtectedUsers : HeatlcheckRuleBase + [RuleModel("A-ProtectedUsers", RiskRuleCategory.Anomalies, RiskModelCategory.PassTheCredential)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 0)] + [RuleSTIG("V-78131", "Accounts with domain level administrative privileges must be members of the Protected Users group in domains with a domain functional level of Windows 2012 R2 or higher.")] + [RuleCERTFR("CERTFR-2017-ALE-012")] + public class HeatlcheckRuleAnomalySchemaProtectedUsers : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalyServicePolicy.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalyServicePolicy.cs index 9277e5a..fc7c8be 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalyServicePolicy.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalyServicePolicy.cs @@ -7,12 +7,13 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-NoServicePolicy", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.WeakPassword)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 0)] - public class HeatlcheckRuleAnomalyServicePolicy : HeatlcheckRuleBase + [RuleModel("A-NoServicePolicy", RiskRuleCategory.Anomalies, RiskModelCategory.WeakPassword)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 0)] + public class HeatlcheckRuleAnomalyServicePolicy : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { @@ -21,17 +22,17 @@ public class HeatlcheckRuleAnomalyServicePolicy : HeatlcheckRuleBase { foreach (GPPSecurityPolicyProperty property in policy.Properties) { - if (!servicePolicy && property.Property == "MinimumPasswordLength") + if (property.Property == "MinimumPasswordLength") { if (property.Value >= 20) { - AddRawDetail(policy.GPOName); + servicePolicy = true; break; } } } } - return null; + return (servicePolicy ? 0 : 1); } } } diff --git a/Healthcheck/Rules/HeatlcheckRuleAnomalySmartCardRequired.cs b/Healthcheck/Rules/HeatlcheckRuleAnomalySmartCardRequired.cs index d6bd259..9f6311a 100644 --- a/Healthcheck/Rules/HeatlcheckRuleAnomalySmartCardRequired.cs +++ b/Healthcheck/Rules/HeatlcheckRuleAnomalySmartCardRequired.cs @@ -7,13 +7,15 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("A-SmartCardRequired", HealthcheckRiskRuleCategory.Anomalies, HealthcheckRiskModelCategory.PassTheCredential)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 30)] - [HeatlcheckRuleSTIG("V-72821")] - public class HeatlcheckRuleAnomalySmartCardRequired : HeatlcheckRuleBase + [RuleModel("A-SmartCardRequired", RiskRuleCategory.Anomalies, RiskModelCategory.PassTheCredential)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 30)] + [RuleSTIG("V-72821", "All accounts, privileged and unprivileged, that require smart cards must have the underlying NT hash rotated at least every 60 days.")] + [RuleANSSI("R38", "paragraph.3.6.2.2")] + public class HeatlcheckRuleAnomalySmartCardRequired : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleAttribute.cs b/Healthcheck/Rules/HeatlcheckRuleAttribute.cs deleted file mode 100644 index 5105aac..0000000 --- a/Healthcheck/Rules/HeatlcheckRuleAttribute.cs +++ /dev/null @@ -1,134 +0,0 @@ -// -// Copyright (c) Ping Castle. All rights reserved. -// https://www.pingcastle.com -// -// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. -// -using System; -using System.Collections.Generic; -using System.Text; - -namespace PingCastle.Healthcheck.Rules -{ - [AttributeUsage(AttributeTargets.Class, Inherited = false)] - public class HeatlcheckRuleModelAttribute : Attribute - { - public HeatlcheckRuleModelAttribute(string Id, HealthcheckRiskRuleCategory Category, HealthcheckRiskModelCategory Model) - { - this.Id = Id; - this.Category = Category; - this.Model = Model; - } - - public string Id { get; private set; } - public HealthcheckRiskRuleCategory Category { get; private set; } - public HealthcheckRiskModelCategory Model { get; private set; } - } - - public enum RuleComputationType - { - TriggerOnThreshold, - TriggerOnPresence, - PerDiscover, - PerDiscoverWithAMinimumOf, - TriggerIfLessThan, - } - - [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] - public class HeatlcheckRuleComputationAttribute : Attribute - { - public HeatlcheckRuleComputationAttribute(RuleComputationType ComputationType, int Score, int Threshold = 0, int Order = 1) - { - this.ComputationType = ComputationType; - this.Score = Score; - this.Threshold = Threshold; - this.Order = Order; - } - - public RuleComputationType ComputationType { get; private set; } - public int Score { get; private set; } - public int Threshold { get; private set; } - public int Order { get; private set; } - - public bool HasMatch(int value, ref int points) - { - switch (ComputationType) - { - case RuleComputationType.TriggerOnPresence: - if (value > 0) - { - points = Score; - return true; - } - return false; - case RuleComputationType.PerDiscoverWithAMinimumOf: - if (value > 0) - { - points = value * Score; - if (points < Threshold) - points = Threshold; - return true; - } - return false; - case RuleComputationType.PerDiscover: - if (value > 0) - { - points = value * Score; - return true; - } - return false; - case RuleComputationType.TriggerOnThreshold: - if (value >= Threshold) - { - points = Score; - return true; - } - return false; - case RuleComputationType.TriggerIfLessThan: - if (value < Threshold) - { - points = Score; - return true; - } - return false; - default: - throw new NotImplementedException(); - } - } - } - - public interface IHeatlcheckRuleFrameworkReference - { - string URL - { - get; - } - string Label - { - get; - } - } - - [AttributeUsage(AttributeTargets.Class, Inherited = false)] - public class HeatlcheckRuleSTIGAttribute : Attribute, IHeatlcheckRuleFrameworkReference - { - public HeatlcheckRuleSTIGAttribute(string id, bool forest = false) - { - ID = id; - ForestCheck = forest; - } - - public string ID { get; private set; } - public bool ForestCheck { get; private set; } - - public string URL { get - { - if (ForestCheck) - return "https://www.stigviewer.com/stig/active_directory_forest/2016-12-19/finding/" + ID; - return "https://www.stigviewer.com/stig/active_directory_domain/2017-12-15/finding/" + ID; - } - } - - public string Label { get { return "STIG " + ID; } } - } -} diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedAdminInactive.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedAdminInactive.cs index 3f05bc3..34c33b2 100644 --- a/Healthcheck/Rules/HeatlcheckRulePrivilegedAdminInactive.cs +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedAdminInactive.cs @@ -7,13 +7,15 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("P-Inactive", HealthcheckRiskRuleCategory.PrivilegedAccounts, HealthcheckRiskModelCategory.AdminControl)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 30, Threshold: 30, Order: 1)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 20, Threshold: 15, Order: 2)] - public class HeatlcheckRulePrivilegedAdminInactive : HeatlcheckRuleBase + [RuleModel("P-Inactive", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.AdminControl)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 30, Threshold: 30, Order: 1)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 20, Threshold: 15, Order: 2)] + [RuleANSSI("R36", "subsection.3.6")] + public class HeatlcheckRulePrivilegedAdminInactive : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedAdminLogin.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedAdminLogin.cs index 9fb6f43..47f0212 100644 --- a/Healthcheck/Rules/HeatlcheckRulePrivilegedAdminLogin.cs +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedAdminLogin.cs @@ -7,12 +7,13 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("P-AdminLogin", HealthcheckRiskRuleCategory.PrivilegedAccounts, HealthcheckRiskModelCategory.AdminControl)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerIfLessThan, 20, Threshold: 35)] - public class HeatlcheckRulePrivilegedAdminLogin : HeatlcheckRuleBase + [RuleModel("P-AdminLogin", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.AdminControl)] + [RuleComputation(RuleComputationType.TriggerIfLessThan, 20, Threshold: 35)] + public class HeatlcheckRulePrivilegedAdminLogin : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedAdminNumber.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedAdminNumber.cs index f10754e..fdacee7 100644 --- a/Healthcheck/Rules/HeatlcheckRulePrivilegedAdminNumber.cs +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedAdminNumber.cs @@ -7,12 +7,15 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("P-AdminNum", HealthcheckRiskRuleCategory.PrivilegedAccounts, HealthcheckRiskModelCategory.AdminControl)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 10, Threshold: 10)] - public class HeatlcheckRulePrivilegedAdminNumber : HeatlcheckRuleBase + [RuleModel("P-AdminNum", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.AdminControl)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 10, Threshold: 10)] + [RuleANSSI("R26", "subsection.3.5")] + [RuleANSSI("R30", "subsubsection.3.5.7")] + public class HeatlcheckRulePrivilegedAdminNumber : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedDCOwner.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedDCOwner.cs index 3c55b3d..067e06c 100644 --- a/Healthcheck/Rules/HeatlcheckRulePrivilegedDCOwner.cs +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedDCOwner.cs @@ -8,12 +8,13 @@ using System.Collections.Generic; using System.Security.Principal; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("P-DCOwner", HealthcheckRiskRuleCategory.PrivilegedAccounts, HealthcheckRiskModelCategory.ACLCheck)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 10)] - public class HeatlcheckRulePrivilegedDCOwner : HeatlcheckRuleBase + [RuleModel("P-DCOwner", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.ACLCheck)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 10)] + public class HeatlcheckRulePrivilegedDCOwner : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedDangerousDelegation.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedDangerousDelegation.cs index ab98edb..7fe59b2 100644 --- a/Healthcheck/Rules/HeatlcheckRulePrivilegedDangerousDelegation.cs +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedDangerousDelegation.cs @@ -7,12 +7,14 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("P-DangerousExtendedRight", HealthcheckRiskRuleCategory.PrivilegedAccounts, HealthcheckRiskModelCategory.ACLCheck)] - [HeatlcheckRuleComputation(RuleComputationType.PerDiscover, 5)] - public class HeatlcheckRulePrivilegedDangerousDelegation : HeatlcheckRuleBase + [RuleModel("P-DangerousExtendedRight", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.ACLCheck)] + [RuleComputation(RuleComputationType.PerDiscover, 5)] + [RuleANSSI("R18", "subsubsection.3.3.2")] + public class HeatlcheckRulePrivilegedDangerousDelegation : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegated.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegated.cs index fb9a2e1..03bb008 100644 --- a/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegated.cs +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegated.cs @@ -7,13 +7,14 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("P-Delegated", HealthcheckRiskRuleCategory.PrivilegedAccounts, HealthcheckRiskModelCategory.ACLCheck)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 20)] - [HeatlcheckRuleSTIG("V-36435")] - public class HeatlcheckRulePrivilegedDelegated : HeatlcheckRuleBase + [RuleModel("P-Delegated", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.ACLCheck)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 20)] + [RuleSTIG("V-36435", "Delegation of privileged accounts must be prohibited.")] + public class HeatlcheckRulePrivilegedDelegated : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationEveryone.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationEveryone.cs index 76d6f0f..f948719 100644 --- a/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationEveryone.cs +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationEveryone.cs @@ -7,18 +7,20 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("P-DelegationEveryone", HealthcheckRiskRuleCategory.PrivilegedAccounts, HealthcheckRiskModelCategory.ACLCheck)] - [HeatlcheckRuleComputation(RuleComputationType.PerDiscover, 15)] - public class HeatlcheckRulePrivilegedDelegationEveryone : HeatlcheckRuleBase + [RuleModel("P-DelegationEveryone", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.ACLCheck)] + [RuleComputation(RuleComputationType.PerDiscover, 15)] + [RuleANSSI("R18", "subsubsection.3.3.2")] + public class HeatlcheckRulePrivilegedDelegationEveryone : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { foreach (HealthcheckDelegationData delegation in healthcheckData.Delegations) { - if (delegation.Account == "Authenticated Users" || delegation.Account == "Everyone") + if (delegation.Account == "Authenticated Users" || delegation.Account == "Everyone" || delegation.Account == "Domain Users" || delegation.Account == "Domain Computers") { AddRawDetail(delegation.DistinguishedName, delegation.Account, delegation.Right); } diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationGPOData.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationGPOData.cs new file mode 100644 index 0000000..735eaa1 --- /dev/null +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationGPOData.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Text; +using PingCastle.Rules; + +namespace PingCastle.Healthcheck.Rules +{ + [RuleModel("P-DelegationGPOData", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.ACLCheck)] + [RuleComputation(RuleComputationType.PerDiscover, 15)] + [RuleANSSI("R18", "subsubsection.3.3.2")] + public class HeatlcheckRulePrivilegedDelegationGPOData : RuleBase + { + protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) + { + foreach (var delegation in healthcheckData.GPODelegation) + { + AddRawDetail(delegation.GPOName, delegation.Item, delegation.Account, delegation.Right); + } + return null; + } + } +} diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationKeyAdmin.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationKeyAdmin.cs new file mode 100644 index 0000000..014d579 --- /dev/null +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationKeyAdmin.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Text; +using PingCastle.Rules; + +namespace PingCastle.Healthcheck.Rules +{ + [RuleModel("P-DelegationKeyAdmin", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.ACLCheck)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] + [RuleANSSI("R18", "subsubsection.3.3.2")] + public class HeatlcheckRulePrivilegedDelegationKeyAdmin : RuleBase + { + protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) + { + foreach (HealthcheckDelegationData delegation in healthcheckData.Delegations) + { + if (delegation.DistinguishedName.StartsWith("DC=") && delegation.SecurityIdentifier.EndsWith("-527")) + { + AddRawDetail(delegation.DistinguishedName, delegation.Account, delegation.Right); + } + } + return null; + } + } +} diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationLoginScript.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationLoginScript.cs index c7845e8..47765f0 100644 --- a/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationLoginScript.cs +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedDelegationLoginScript.cs @@ -7,12 +7,14 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("P-DelegationLoginScript", HealthcheckRiskRuleCategory.PrivilegedAccounts, HealthcheckRiskModelCategory.ACLCheck)] - [HeatlcheckRuleComputation(RuleComputationType.PerDiscover, 15)] - public class HeatlcheckRulePrivilegedDelegationLoginScript : HeatlcheckRuleBase + [RuleModel("P-DelegationLoginScript", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.ACLCheck)] + [RuleComputation(RuleComputationType.PerDiscover, 15)] + [RuleANSSI("R18", "subsubsection.3.3.2")] + public class HeatlcheckRulePrivilegedDelegationLoginScript : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedExchangeAdminSDHolder.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedExchangeAdminSDHolder.cs new file mode 100644 index 0000000..a7df5b4 --- /dev/null +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedExchangeAdminSDHolder.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Text; +using PingCastle.Rules; + +namespace PingCastle.Healthcheck.Rules +{ + [RuleModel("P-ExchangeAdminSDHolder", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.ACLCheck)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] + [RuleANSSI("R18", "subsubsection.3.3.2")] + public class HeatlcheckRulePrivilegedExchangeAdminSDHolder : RuleBase + { + protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) + { + foreach (HealthcheckDelegationData delegation in healthcheckData.Delegations) + { + if (delegation.DistinguishedName == "AdminSDHolder" + && delegation.Account.Contains("Exchange")) + { + return 1; + } + } + return 0; + } + } +} diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedOperatorsEmpty.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedOperatorsEmpty.cs new file mode 100644 index 0000000..5275070 --- /dev/null +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedOperatorsEmpty.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Text; +using PingCastle.Rules; + +namespace PingCastle.Healthcheck.Rules +{ + [RuleModel("P-OperatorsEmpty", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.AdminControl)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 0)] + [RuleANSSI("R27", "subsection.3.5")] + public class HeatlcheckRulePrivilegedOperatorsEmpty : RuleBase + { + protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) + { + foreach(var group in healthcheckData.PrivilegedGroups) + { + if (group.GroupName == "Account Operators" || group.GroupName == "Server Operators") + { + if (group.NumberOfMember > 0) + { + AddRawDetail(group.GroupName, group.NumberOfMember); + } + } + } + return null; + } + } +} diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedPrivilegeEveryone.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedPrivilegeEveryone.cs new file mode 100644 index 0000000..0677543 --- /dev/null +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedPrivilegeEveryone.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Text; +using PingCastle.Rules; + +namespace PingCastle.Healthcheck.Rules +{ + [RuleModel("P-PrivilegeEveryone", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.ACLCheck)] + [RuleComputation(RuleComputationType.PerDiscover, 15)] + [RuleANSSI("R18", "subsubsection.3.3.2")] + public class HeatlcheckRulePrivilegedPrivilegeEveryone : RuleBase + { + protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) + { + var dangerousPrivileges = new List() + { + "SeLoadDriverPrivilege", + "SeTcbPrivilege", + "SeDebugPrivilege", + "SeRestorePrivilege", + "SeBackupPrivilege", + "SeTakeOwnershipPrivilege", + "SeCreateTokenPrivilege", + "SeImpersonatePrivilege", + "SeAssignPrimaryTokenPrivilege", + }; + foreach (var privilege in healthcheckData.GPPRightAssignment) + { + if (!dangerousPrivileges.Contains(privilege.Privilege)) + continue; + if (privilege.User == "Authenticated Users" || privilege.User == "Everyone" || privilege.User == "Domain Users" || privilege.User == "Domain Computers") + { + AddRawDetail(privilege.GPOName, privilege.User, privilege.Privilege); + } + } + return null; + } + } +} diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedSchemaAdmins.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedSchemaAdmins.cs index d461980..7e2d241 100644 --- a/Healthcheck/Rules/HeatlcheckRulePrivilegedSchemaAdmins.cs +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedSchemaAdmins.cs @@ -7,13 +7,15 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("P-SchemaAdmin", HealthcheckRiskRuleCategory.PrivilegedAccounts, HealthcheckRiskModelCategory.IrreversibleChange)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 10)] - [HeatlcheckRuleSTIG("V-72835", true)] - public class HeatlcheckRulePrivilegedSchemaAdmins : HeatlcheckRuleBase + [RuleModel("P-SchemaAdmin", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.IrreversibleChange)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 10)] + [RuleSTIG("V-72835", "Membership to the Schema Admins group must be limited", true)] + [RuleANSSI("R13", "subsection.3.2")] + public class HeatlcheckRulePrivilegedSchemaAdmins : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedServiceDomainAdmin.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedServiceDomainAdmin.cs index cbd407e..df67fc9 100644 --- a/Healthcheck/Rules/HeatlcheckRulePrivilegedServiceDomainAdmin.cs +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedServiceDomainAdmin.cs @@ -8,13 +8,15 @@ using System.Collections.Generic; using System.Diagnostics; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("P-ServiceDomainAdmin", HealthcheckRiskRuleCategory.PrivilegedAccounts, HealthcheckRiskModelCategory.PrivilegeControl)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 15, Threshold: 2)] - [HeatlcheckRuleSTIG("V-36432")] - public class HeatlcheckRulePrivilegedServiceDomainAdmin : HeatlcheckRuleBase + [RuleModel("P-ServiceDomainAdmin", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.PrivilegeControl)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 15, Threshold: 2)] + [RuleSTIG("V-36432", "Membership to the Domain Admins group must be restricted to accounts used only to manage the Active Directory domain and domain controllers.")] + [RuleANSSI("R11", "subsection.2.5")] + public class HeatlcheckRulePrivilegedServiceDomainAdmin : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedUnconstrainedDelegation.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedUnconstrainedDelegation.cs new file mode 100644 index 0000000..e4e22a6 --- /dev/null +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedUnconstrainedDelegation.cs @@ -0,0 +1,48 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Text; +using PingCastle.Rules; + +namespace PingCastle.Healthcheck.Rules +{ + [RuleModel("P-UnconstrainedDelegation", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.ACLCheck)] + [RuleComputation(RuleComputationType.PerDiscover, 5)] + [RuleANSSI("R18", "subsubsection.3.3.2")] + public class HeatlcheckRulePrivilegedUnconstrainedDelegation : RuleBase + { + protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) + { + if (healthcheckData.UserAccountData.ListTrustedToAuthenticateForDelegation != null) + { + foreach (var delegation in healthcheckData.UserAccountData.ListTrustedToAuthenticateForDelegation) + { + AddRawDetail(delegation.DistinguishedName, delegation.Name); + } + } + if (healthcheckData.ComputerAccountData.ListTrustedToAuthenticateForDelegation != null) + { + foreach (var delegation in healthcheckData.ComputerAccountData.ListTrustedToAuthenticateForDelegation) + { + bool found = false; + foreach (var dc in healthcheckData.DomainControllers) + { + if (dc.DistinguishedName == delegation.DistinguishedName) + { + found = true; + break; + } + } + if (!found) + AddRawDetail(delegation.DistinguishedName, delegation.Name); + } + } + return null; + } + } +} diff --git a/Healthcheck/Rules/HeatlcheckRulePrivilegedUnknownDelegation.cs b/Healthcheck/Rules/HeatlcheckRulePrivilegedUnknownDelegation.cs index 0ba0bc5..1fe4bb5 100644 --- a/Healthcheck/Rules/HeatlcheckRulePrivilegedUnknownDelegation.cs +++ b/Healthcheck/Rules/HeatlcheckRulePrivilegedUnknownDelegation.cs @@ -7,12 +7,13 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("P-UnkownDelegation", HealthcheckRiskRuleCategory.PrivilegedAccounts, HealthcheckRiskModelCategory.ACLCheck)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 15)] - public class HeatlcheckRulePrivilegedUnknownDelegation : HeatlcheckRuleBase + [RuleModel("P-UnkownDelegation", RiskRuleCategory.PrivilegedAccounts, RiskModelCategory.ACLCheck)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 15)] + public class HeatlcheckRulePrivilegedUnknownDelegation : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleStaleADRegistrationEnabled.cs b/Healthcheck/Rules/HeatlcheckRuleStaleADRegistrationEnabled.cs index 9adcefd..826617e 100644 --- a/Healthcheck/Rules/HeatlcheckRuleStaleADRegistrationEnabled.cs +++ b/Healthcheck/Rules/HeatlcheckRuleStaleADRegistrationEnabled.cs @@ -8,12 +8,13 @@ using System.Collections.Generic; using System.Diagnostics; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("S-ADRegistration", HealthcheckRiskRuleCategory.StaleObjects, HealthcheckRiskModelCategory.Provisioning)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 10)] - public class HeatlcheckRuleStaleADRegistrationEnabled : HeatlcheckRuleBase + [RuleModel("S-ADRegistration", RiskRuleCategory.StaleObjects, RiskModelCategory.Provisioning)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 10)] + public class HeatlcheckRuleStaleADRegistrationEnabled : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { @@ -27,7 +28,7 @@ public class HeatlcheckRuleStaleADRegistrationEnabled : HeatlcheckRuleBase || right.User == "Authenticated Users") { Trace.WriteLine("SeMachineAccountPrivilege found in GPO " + right.GPOName); - return 1; + return healthcheckData.MachineAccountQuota; } } } diff --git a/Healthcheck/Rules/HeatlcheckRuleStaledDCNotUpdated.cs b/Healthcheck/Rules/HeatlcheckRuleStaledDCNotUpdated.cs index 8a05dd6..b471a63 100644 --- a/Healthcheck/Rules/HeatlcheckRuleStaledDCNotUpdated.cs +++ b/Healthcheck/Rules/HeatlcheckRuleStaledDCNotUpdated.cs @@ -7,12 +7,14 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("S-DC-NotUpdated", HealthcheckRiskRuleCategory.StaleObjects, HealthcheckRiskModelCategory.VulnerabilityManagement)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 15)] - public class HeatlcheckRuleStaledDCNotRebooted : HeatlcheckRuleBase + [RuleModel("S-DC-NotUpdated", RiskRuleCategory.StaleObjects, RiskModelCategory.VulnerabilityManagement)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 15)] + [RuleBSI("M 4.315")] + public class HeatlcheckRuleStaledDCNotRebooted : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleStaledDCSubnetMissing.cs b/Healthcheck/Rules/HeatlcheckRuleStaledDCSubnetMissing.cs index 3a2a25c..9841cf6 100644 --- a/Healthcheck/Rules/HeatlcheckRuleStaledDCSubnetMissing.cs +++ b/Healthcheck/Rules/HeatlcheckRuleStaledDCSubnetMissing.cs @@ -8,12 +8,13 @@ using System.Collections.Generic; using System.Net; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("S-DC-SubnetMissing", HealthcheckRiskRuleCategory.StaleObjects, HealthcheckRiskModelCategory.NetworkTopography)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 5)] - public class HeatlcheckRuleStaledDCSubnetMissing : HeatlcheckRuleBase + [RuleModel("S-DC-SubnetMissing", RiskRuleCategory.StaleObjects, RiskModelCategory.NetworkTopography)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] + public class HeatlcheckRuleStaledDCSubnetMissing : RuleBase { private class Subnet { @@ -84,6 +85,8 @@ public bool MatchIp(IPAddress ipaddress) foreach (string ip in dc.IP) { var ipaddress = IPAddress.Parse(ip); + if (ipaddress.IsIPv6LinkLocal || ipaddress.IsIPv6Multicast || ipaddress.IsIPv6SiteLocal) + continue; bool found = false; foreach (var subnet in subnets) { diff --git a/Healthcheck/Rules/HeatlcheckRuleStaledDesEnabled.cs b/Healthcheck/Rules/HeatlcheckRuleStaledDesEnabled.cs index d4bfa8c..73e48d3 100644 --- a/Healthcheck/Rules/HeatlcheckRuleStaledDesEnabled.cs +++ b/Healthcheck/Rules/HeatlcheckRuleStaledDesEnabled.cs @@ -7,16 +7,17 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("S-DesEnabled", HealthcheckRiskRuleCategory.StaleObjects, HealthcheckRiskModelCategory.OldAuthenticationProtocols)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 15)] - public class HeatlcheckRuleStaledDesEnabled : HeatlcheckRuleBase + [RuleModel("S-DesEnabled", RiskRuleCategory.StaleObjects, RiskModelCategory.OldAuthenticationProtocols)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 15)] + public class HeatlcheckRuleStaledDesEnabled : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { - return healthcheckData.UserAccountData.NumberDesEnabled; + return healthcheckData.UserAccountData.NumberDesEnabled + healthcheckData.ComputerAccountData.NumberDesEnabled; } } } diff --git a/Healthcheck/Rules/HeatlcheckRuleStaledDuplicateObjects.cs b/Healthcheck/Rules/HeatlcheckRuleStaledDuplicateObjects.cs index 334ab4a..96dc4aa 100644 --- a/Healthcheck/Rules/HeatlcheckRuleStaledDuplicateObjects.cs +++ b/Healthcheck/Rules/HeatlcheckRuleStaledDuplicateObjects.cs @@ -7,12 +7,13 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("S-Duplicate", HealthcheckRiskRuleCategory.StaleObjects, HealthcheckRiskModelCategory.Replication)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 5)] - public class HeatlcheckRuleStaledDuplicateObjects : HeatlcheckRuleBase + [RuleModel("S-Duplicate", RiskRuleCategory.StaleObjects, RiskModelCategory.Replication)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] + public class HeatlcheckRuleStaledDuplicateObjects : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleStaledInactive.cs b/Healthcheck/Rules/HeatlcheckRuleStaledInactive.cs index f5cf22e..4277be9 100644 --- a/Healthcheck/Rules/HeatlcheckRuleStaledInactive.cs +++ b/Healthcheck/Rules/HeatlcheckRuleStaledInactive.cs @@ -7,12 +7,14 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("S-Inactive", HealthcheckRiskRuleCategory.StaleObjects, HealthcheckRiskModelCategory.InactiveUserOrComputer)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 10, Threshold: 15)] - public class HeatlcheckRuleStaledInactive : HeatlcheckRuleBase + [RuleModel("S-Inactive", RiskRuleCategory.StaleObjects, RiskModelCategory.InactiveUserOrComputer)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 10, Threshold: 15)] + [RuleANSSI("R45", "paragraph.3.6.6.2")] + public class HeatlcheckRuleStaledInactive : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleStaledInactiveComputer.cs b/Healthcheck/Rules/HeatlcheckRuleStaledInactiveComputer.cs index 50dc637..d19d874 100644 --- a/Healthcheck/Rules/HeatlcheckRuleStaledInactiveComputer.cs +++ b/Healthcheck/Rules/HeatlcheckRuleStaledInactiveComputer.cs @@ -7,14 +7,16 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("S-C-Inactive", HealthcheckRiskRuleCategory.StaleObjects, HealthcheckRiskModelCategory.InactiveUserOrComputer)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 30, Threshold: 30, Order: 1)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 10, Threshold: 20, Order: 2)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 5, Threshold: 15, Order: 3)] - public class HeatlcheckRuleStaledInactiveComputer : HeatlcheckRuleBase + [RuleModel("S-C-Inactive", RiskRuleCategory.StaleObjects, RiskModelCategory.InactiveUserOrComputer)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 30, Threshold: 30, Order: 1)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 10, Threshold: 20, Order: 2)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 5, Threshold: 15, Order: 3)] + [RuleANSSI("R45", "paragraph.3.6.6.2")] + public class HeatlcheckRuleStaledInactiveComputer : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleStaledMS14_068.cs b/Healthcheck/Rules/HeatlcheckRuleStaledMS14_068.cs index 97d70ff..a37c775 100644 --- a/Healthcheck/Rules/HeatlcheckRuleStaledMS14_068.cs +++ b/Healthcheck/Rules/HeatlcheckRuleStaledMS14_068.cs @@ -7,12 +7,14 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("S-Vuln-MS14-068", HealthcheckRiskRuleCategory.StaleObjects, HealthcheckRiskModelCategory.VulnerabilityManagement)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 100)] - public class HeatlcheckRuleStaledMS14_068 : HeatlcheckRuleBase + [RuleModel("S-Vuln-MS14-068", RiskRuleCategory.StaleObjects, RiskModelCategory.VulnerabilityManagement)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 100)] + [RuleCERTFR("CERTFR-2014-ALE-011")] + public class HeatlcheckRuleStaledMS14_068 : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleStaledMS17_010.cs b/Healthcheck/Rules/HeatlcheckRuleStaledMS17_010.cs index e3ab4b3..c4e6c99 100644 --- a/Healthcheck/Rules/HeatlcheckRuleStaledMS17_010.cs +++ b/Healthcheck/Rules/HeatlcheckRuleStaledMS17_010.cs @@ -7,12 +7,14 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("S-Vuln-MS17_010", HealthcheckRiskRuleCategory.StaleObjects, HealthcheckRiskModelCategory.VulnerabilityManagement)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 100)] - public class HeatlcheckRuleStaledMS17_010 : HeatlcheckRuleBase + [RuleModel("S-Vuln-MS17_010", RiskRuleCategory.StaleObjects, RiskModelCategory.VulnerabilityManagement)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 100)] + [RuleCERTFR("CERTFR-2017-ALE-010")] + public class HeatlcheckRuleStaledMS17_010 : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleStaledNoPreAuth.cs b/Healthcheck/Rules/HeatlcheckRuleStaledNoPreAuth.cs index 972bfd8..022c427 100644 --- a/Healthcheck/Rules/HeatlcheckRuleStaledNoPreAuth.cs +++ b/Healthcheck/Rules/HeatlcheckRuleStaledNoPreAuth.cs @@ -7,12 +7,13 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("S-NoPreAuth", HealthcheckRiskRuleCategory.StaleObjects, HealthcheckRiskModelCategory.ObjectConfig)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 5)] - public class HeatlcheckRuleStaledNoPreAuth : HeatlcheckRuleBase + [RuleModel("S-NoPreAuth", RiskRuleCategory.StaleObjects, RiskModelCategory.ObjectConfig)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] + public class HeatlcheckRuleStaledNoPreAuth : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleStaledObsolete2000.cs b/Healthcheck/Rules/HeatlcheckRuleStaledObsolete2000.cs index 62167d6..d079576 100644 --- a/Healthcheck/Rules/HeatlcheckRuleStaledObsolete2000.cs +++ b/Healthcheck/Rules/HeatlcheckRuleStaledObsolete2000.cs @@ -7,12 +7,14 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("S-OS-2000", HealthcheckRiskRuleCategory.StaleObjects, HealthcheckRiskModelCategory.ObsoleteOS)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 40)] - public class HeatlcheckRuleStaledObsolete2000 : HeatlcheckRuleBase + [RuleModel("S-OS-2000", RiskRuleCategory.StaleObjects, RiskModelCategory.ObsoleteOS)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 40)] + [RuleCERTFR("CERTFR-2005-INF-003", "SECTION00032400000000000000")] + public class HeatlcheckRuleStaledObsolete2000 : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleStaledObsolete2003.cs b/Healthcheck/Rules/HeatlcheckRuleStaledObsolete2003.cs index f33bb89..90ce9c9 100644 --- a/Healthcheck/Rules/HeatlcheckRuleStaledObsolete2003.cs +++ b/Healthcheck/Rules/HeatlcheckRuleStaledObsolete2003.cs @@ -7,14 +7,16 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("S-OS-2003", HealthcheckRiskRuleCategory.StaleObjects, HealthcheckRiskModelCategory.ObsoleteOS)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 30, Threshold: 15, Order: 1)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 25, Threshold: 6, Order: 2)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 20, Order: 3)] - public class HeatlcheckRuleStaledObsolete2003 : HeatlcheckRuleBase + [RuleModel("S-OS-2003", RiskRuleCategory.StaleObjects, RiskModelCategory.ObsoleteOS)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 30, Threshold: 15, Order: 1)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 25, Threshold: 6, Order: 2)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 20, Order: 3)] + [RuleCERTFR("CERTFR-2005-INF-003", "SECTION00032400000000000000")] + public class HeatlcheckRuleStaledObsolete2003 : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleStaledObsoleteDC2000.cs b/Healthcheck/Rules/HeatlcheckRuleStaledObsoleteDC2000.cs index bf71803..960f41b 100644 --- a/Healthcheck/Rules/HeatlcheckRuleStaledObsoleteDC2000.cs +++ b/Healthcheck/Rules/HeatlcheckRuleStaledObsoleteDC2000.cs @@ -7,13 +7,15 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("S-DC-2000", HealthcheckRiskRuleCategory.StaleObjects, HealthcheckRiskModelCategory.ObsoleteOS)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 40)] - [HeatlcheckRuleSTIG("V-8551")] - public class HeatlcheckRuleStaledObsoleteDC2000 : HeatlcheckRuleBase + [RuleModel("S-DC-2000", RiskRuleCategory.StaleObjects, RiskModelCategory.ObsoleteOS)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 40)] + [RuleSTIG("V-8551", "The domain functional level must be at a Windows Server version still supported by Microsoft.")] + [RuleANSSI("R12", "subsection.3.1")] + public class HeatlcheckRuleStaledObsoleteDC2000 : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleStaledObsoleteDC2003.cs b/Healthcheck/Rules/HeatlcheckRuleStaledObsoleteDC2003.cs index f690204..9f577b4 100644 --- a/Healthcheck/Rules/HeatlcheckRuleStaledObsoleteDC2003.cs +++ b/Healthcheck/Rules/HeatlcheckRuleStaledObsoleteDC2003.cs @@ -7,13 +7,15 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("S-DC-2003", HealthcheckRiskRuleCategory.StaleObjects, HealthcheckRiskModelCategory.ObsoleteOS)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 20)] - [HeatlcheckRuleSTIG("V-8551")] - public class HeatlcheckRuleStaledObsoleteDC2003 : HeatlcheckRuleBase + [RuleModel("S-DC-2003", RiskRuleCategory.StaleObjects, RiskModelCategory.ObsoleteOS)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 20)] + [RuleSTIG("V-8551", "The domain functional level must be at a Windows Server version still supported by Microsoft.")] + [RuleANSSI("R12", "subsection.3.1")] + public class HeatlcheckRuleStaledObsoleteDC2003 : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleStaledObsoleteNT4.cs b/Healthcheck/Rules/HeatlcheckRuleStaledObsoleteNT4.cs index 89c1d71..9d0eb05 100644 --- a/Healthcheck/Rules/HeatlcheckRuleStaledObsoleteNT4.cs +++ b/Healthcheck/Rules/HeatlcheckRuleStaledObsoleteNT4.cs @@ -7,12 +7,14 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("S-OS-NT", HealthcheckRiskRuleCategory.StaleObjects, HealthcheckRiskModelCategory.ObsoleteOS)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 60)] - public class HeatlcheckRuleStaledObsoleteNT4 : HeatlcheckRuleBase + [RuleModel("S-OS-NT", RiskRuleCategory.StaleObjects, RiskModelCategory.ObsoleteOS)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 60)] + [RuleCERTFR("CERTFR-2005-INF-003", "SECTION00032400000000000000")] + public class HeatlcheckRuleStaledObsoleteNT4 : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleStaledObsoleteXP.cs b/Healthcheck/Rules/HeatlcheckRuleStaledObsoleteXP.cs index a869d03..f347072 100644 --- a/Healthcheck/Rules/HeatlcheckRuleStaledObsoleteXP.cs +++ b/Healthcheck/Rules/HeatlcheckRuleStaledObsoleteXP.cs @@ -7,14 +7,16 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("S-OS-XP", HealthcheckRiskRuleCategory.StaleObjects, HealthcheckRiskModelCategory.ObsoleteOS)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 20, Threshold: 15, Order: 1)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 15, Threshold: 6, Order: 2)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 10, Order: 3)] - public class HeatlcheckRuleStaledObsoleteXP : HeatlcheckRuleBase + [RuleModel("S-OS-XP", RiskRuleCategory.StaleObjects, RiskModelCategory.ObsoleteOS)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 20, Threshold: 15, Order: 1)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 15, Threshold: 6, Order: 2)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 10, Order: 3)] + [RuleCERTFR("CERTFR-2005-INF-003", "SECTION00032400000000000000")] + public class HeatlcheckRuleStaledObsoleteXP : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleStaledPrimaryGroup.cs b/Healthcheck/Rules/HeatlcheckRuleStaledPrimaryGroup.cs index 935213a..7d17ce6 100644 --- a/Healthcheck/Rules/HeatlcheckRuleStaledPrimaryGroup.cs +++ b/Healthcheck/Rules/HeatlcheckRuleStaledPrimaryGroup.cs @@ -7,12 +7,13 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("S-PrimaryGroup", HealthcheckRiskRuleCategory.StaleObjects, HealthcheckRiskModelCategory.ObjectConfig)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 15)] - public class HeatlcheckRuleStaledPrimaryGroup : HeatlcheckRuleBase + [RuleModel("S-PrimaryGroup", RiskRuleCategory.StaleObjects, RiskModelCategory.ObjectConfig)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 15)] + public class HeatlcheckRuleStaledPrimaryGroup : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleStaledPrimaryGroupComputer.cs b/Healthcheck/Rules/HeatlcheckRuleStaledPrimaryGroupComputer.cs index 0553e30..2622b29 100644 --- a/Healthcheck/Rules/HeatlcheckRuleStaledPrimaryGroupComputer.cs +++ b/Healthcheck/Rules/HeatlcheckRuleStaledPrimaryGroupComputer.cs @@ -7,12 +7,13 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("S-C-PrimaryGroup", HealthcheckRiskRuleCategory.StaleObjects, HealthcheckRiskModelCategory.ObjectConfig)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 15)] - public class HeatlcheckRuleStaledPrimaryGroupComputer : HeatlcheckRuleBase + [RuleModel("S-C-PrimaryGroup", RiskRuleCategory.StaleObjects, RiskModelCategory.ObjectConfig)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 15)] + public class HeatlcheckRuleStaledPrimaryGroupComputer : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleStaledPwdNotRequired.cs b/Healthcheck/Rules/HeatlcheckRuleStaledPwdNotRequired.cs index 1788cfc..d366bb3 100644 --- a/Healthcheck/Rules/HeatlcheckRuleStaledPwdNotRequired.cs +++ b/Healthcheck/Rules/HeatlcheckRuleStaledPwdNotRequired.cs @@ -7,12 +7,14 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("S-PwdNotRequired", HealthcheckRiskRuleCategory.StaleObjects, HealthcheckRiskModelCategory.ObjectConfig)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 15)] - public class HeatlcheckRuleStaledPwdNotRequired : HeatlcheckRuleBase + [RuleModel("S-PwdNotRequired", RiskRuleCategory.StaleObjects, RiskModelCategory.ObjectConfig)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 15)] + [RuleANSSI("R36", "subsection.3.6")] + public class HeatlcheckRuleStaledPwdNotRequired : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleStaledReversibleEncryption.cs b/Healthcheck/Rules/HeatlcheckRuleStaledReversibleEncryption.cs index f0f15c8..2c88c7b 100644 --- a/Healthcheck/Rules/HeatlcheckRuleStaledReversibleEncryption.cs +++ b/Healthcheck/Rules/HeatlcheckRuleStaledReversibleEncryption.cs @@ -7,12 +7,13 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("S-Reversible", HealthcheckRiskRuleCategory.StaleObjects, HealthcheckRiskModelCategory.ObjectConfig)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 5)] - public class HeatlcheckRuleStaledReversibleEncryption : HeatlcheckRuleBase + [RuleModel("S-Reversible", RiskRuleCategory.StaleObjects, RiskModelCategory.ObjectConfig)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] + public class HeatlcheckRuleStaledReversibleEncryption : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleStaledReversibleEncryptionComputer.cs b/Healthcheck/Rules/HeatlcheckRuleStaledReversibleEncryptionComputer.cs index e2b87cd..3ac24cc 100644 --- a/Healthcheck/Rules/HeatlcheckRuleStaledReversibleEncryptionComputer.cs +++ b/Healthcheck/Rules/HeatlcheckRuleStaledReversibleEncryptionComputer.cs @@ -7,12 +7,13 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("S-C-Reversible", HealthcheckRiskRuleCategory.StaleObjects, HealthcheckRiskModelCategory.ObjectConfig)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 5)] - public class HeatlcheckRuleStaledReversibleEncryptionComputer : HeatlcheckRuleBase + [RuleModel("S-C-Reversible", RiskRuleCategory.StaleObjects, RiskModelCategory.ObjectConfig)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] + public class HeatlcheckRuleStaledReversibleEncryptionComputer : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleStaledSIDHistory.cs b/Healthcheck/Rules/HeatlcheckRuleStaledSIDHistory.cs index b065fe2..f1ead1b 100644 --- a/Healthcheck/Rules/HeatlcheckRuleStaledSIDHistory.cs +++ b/Healthcheck/Rules/HeatlcheckRuleStaledSIDHistory.cs @@ -7,12 +7,15 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Data; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("S-SIDHistory", HealthcheckRiskRuleCategory.StaleObjects, HealthcheckRiskModelCategory.ObjectConfig)] - [HeatlcheckRuleComputation(RuleComputationType.PerDiscoverWithAMinimumOf, 5, Threshold: 15)] - public class HeatlcheckRuleStaledSIDHistory : HeatlcheckRuleBase + [RuleModel("S-SIDHistory", RiskRuleCategory.StaleObjects, RiskModelCategory.ObjectConfig)] + [RuleComputation(RuleComputationType.PerDiscoverWithAMinimumOf, 5, Threshold: 15)] + [RuleANSSI("R15", "paragraph.3.3.1.5")] + public class HeatlcheckRuleStaledSIDHistory : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData, ICollection SourceDomains) { diff --git a/Healthcheck/Rules/HeatlcheckRuleStaledSMBv1.cs b/Healthcheck/Rules/HeatlcheckRuleStaledSMBv1.cs index 3d696b1..5fac709 100644 --- a/Healthcheck/Rules/HeatlcheckRuleStaledSMBv1.cs +++ b/Healthcheck/Rules/HeatlcheckRuleStaledSMBv1.cs @@ -7,12 +7,16 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("S-SMB-v1", HealthcheckRiskRuleCategory.StaleObjects, HealthcheckRiskModelCategory.OldAuthenticationProtocols)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 1)] - public class HeatlcheckRuleStaledSMBv1 : HeatlcheckRuleBase + [RuleModel("S-SMB-v1", RiskRuleCategory.StaleObjects, RiskModelCategory.OldAuthenticationProtocols)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 1)] + [RuleBSI("M 2.412")] + [RuleCERTFR("CERTFR-2017-ACT-019", "SECTION00010000000000000000")] + [RuleCERTFR("CERTFR-2016-ACT-039", "SECTION00010000000000000000")] + public class HeatlcheckRuleStaledSMBv1 : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleTrustDownlevel.cs b/Healthcheck/Rules/HeatlcheckRuleTrustDownlevel.cs index d6ba055..aff94a2 100644 --- a/Healthcheck/Rules/HeatlcheckRuleTrustDownlevel.cs +++ b/Healthcheck/Rules/HeatlcheckRuleTrustDownlevel.cs @@ -7,12 +7,13 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("T-Downlevel", HealthcheckRiskRuleCategory.Trusts, HealthcheckRiskModelCategory.OldTrustProtocol)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 20)] - public class HeatlcheckRuleTrustDownlevel : HeatlcheckRuleBase + [RuleModel("T-Downlevel", RiskRuleCategory.Trusts, RiskModelCategory.OldTrustProtocol)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 20)] + public class HeatlcheckRuleTrustDownlevel : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleTrustInactive.cs b/Healthcheck/Rules/HeatlcheckRuleTrustInactive.cs index d751f05..519b0d7 100644 --- a/Healthcheck/Rules/HeatlcheckRuleTrustInactive.cs +++ b/Healthcheck/Rules/HeatlcheckRuleTrustInactive.cs @@ -7,12 +7,13 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("T-Inactive", HealthcheckRiskRuleCategory.Trusts, HealthcheckRiskModelCategory.TrustInactive)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 20)] - public class HeatlcheckRuleTrustInactive : HeatlcheckRuleBase + [RuleModel("T-Inactive", RiskRuleCategory.Trusts, RiskModelCategory.TrustInactive)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 20)] + public class HeatlcheckRuleTrustInactive : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleTrustLoginScriptOutOfDomain.cs b/Healthcheck/Rules/HeatlcheckRuleTrustLoginScriptOutOfDomain.cs index c9d0148..8ddbb91 100644 --- a/Healthcheck/Rules/HeatlcheckRuleTrustLoginScriptOutOfDomain.cs +++ b/Healthcheck/Rules/HeatlcheckRuleTrustLoginScriptOutOfDomain.cs @@ -8,12 +8,13 @@ using System.Collections.Generic; using System.Diagnostics; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("T-ScriptOutOfDomain", HealthcheckRiskRuleCategory.Trusts, HealthcheckRiskModelCategory.TrustImpermeability)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 10)] - public class HeatlcheckRuleTrustLoginScriptOutOfDomain : HeatlcheckRuleBase + [RuleModel("T-ScriptOutOfDomain", RiskRuleCategory.Trusts, RiskModelCategory.TrustImpermeability)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 10)] + public class HeatlcheckRuleTrustLoginScriptOutOfDomain : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleTrustSIDFiltering.cs b/Healthcheck/Rules/HeatlcheckRuleTrustSIDFiltering.cs index 1076de8..b48bbe1 100644 --- a/Healthcheck/Rules/HeatlcheckRuleTrustSIDFiltering.cs +++ b/Healthcheck/Rules/HeatlcheckRuleTrustSIDFiltering.cs @@ -7,15 +7,19 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Data; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("T-SIDFiltering", HealthcheckRiskRuleCategory.Trusts, HealthcheckRiskModelCategory.SIDFiltering)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 100, Threshold: 4, Order: 1)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnThreshold, 80, Threshold: 2, Order: 2)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 50, Order: 3)] - [HeatlcheckRuleSTIG("V-8538")] - public class HeatlcheckRuleTrustSIDFiltering : HeatlcheckRuleBase + [RuleModel("T-SIDFiltering", RiskRuleCategory.Trusts, RiskModelCategory.SIDFiltering)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 100, Threshold: 4, Order: 1)] + [RuleComputation(RuleComputationType.TriggerOnThreshold, 80, Threshold: 2, Order: 2)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 50, Order: 3)] + [RuleSTIG("V-8538", "Security identifiers (SIDs) must be configured to use only authentication data of directly trusted external or forest trust. ")] + [RuleANSSI("R16", "paragraph.3.3.1.6")] + [RuleBSI("M 4.314")] + public class HeatlcheckRuleTrustSIDFiltering : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData, ICollection AllowedMigrationDomains) { diff --git a/Healthcheck/Rules/HeatlcheckRuleTrustSIDHistoryAuditingGroup.cs b/Healthcheck/Rules/HeatlcheckRuleTrustSIDHistoryAuditingGroup.cs index fab99a0..af0d459 100644 --- a/Healthcheck/Rules/HeatlcheckRuleTrustSIDHistoryAuditingGroup.cs +++ b/Healthcheck/Rules/HeatlcheckRuleTrustSIDHistoryAuditingGroup.cs @@ -7,12 +7,14 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("S-Domain$$$", HealthcheckRiskRuleCategory.Trusts, HealthcheckRiskModelCategory.SIDHistory)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 5)] - public class HeatlcheckRuleTrustSIDHistoryAuditingGroup : HeatlcheckRuleBase + [RuleModel("S-Domain$$$", RiskRuleCategory.Trusts, RiskModelCategory.SIDHistory)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 5)] + [RuleANSSI("R15", "paragraph.3.3.1.5")] + public class HeatlcheckRuleTrustSIDHistoryAuditingGroup : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleTrustSIDHistorySameDomain.cs b/Healthcheck/Rules/HeatlcheckRuleTrustSIDHistorySameDomain.cs index 5f8ae5e..cceacbf 100644 --- a/Healthcheck/Rules/HeatlcheckRuleTrustSIDHistorySameDomain.cs +++ b/Healthcheck/Rules/HeatlcheckRuleTrustSIDHistorySameDomain.cs @@ -7,12 +7,14 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("T-SIDHistorySameDomain", HealthcheckRiskRuleCategory.Trusts, HealthcheckRiskModelCategory.SIDHistory)] - [HeatlcheckRuleComputation(RuleComputationType.TriggerOnPresence, 50)] - public class HeatlcheckRuleTrustSIDHistorySameDomain : HeatlcheckRuleBase + [RuleModel("T-SIDHistorySameDomain", RiskRuleCategory.Trusts, RiskModelCategory.SIDHistory)] + [RuleComputation(RuleComputationType.TriggerOnPresence, 50)] + [RuleANSSI("R15", "paragraph.3.3.1.5")] + public class HeatlcheckRuleTrustSIDHistorySameDomain : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/HeatlcheckRuleTrustSIDHistoryUnknownDomain.cs b/Healthcheck/Rules/HeatlcheckRuleTrustSIDHistoryUnknownDomain.cs index 1c0b4e8..b605ebf 100644 --- a/Healthcheck/Rules/HeatlcheckRuleTrustSIDHistoryUnknownDomain.cs +++ b/Healthcheck/Rules/HeatlcheckRuleTrustSIDHistoryUnknownDomain.cs @@ -7,12 +7,14 @@ using System; using System.Collections.Generic; using System.Text; +using PingCastle.Rules; namespace PingCastle.Healthcheck.Rules { - [HeatlcheckRuleModel("T-SIDHistoryUnknownDomain", HealthcheckRiskRuleCategory.Trusts, HealthcheckRiskModelCategory.SIDHistory)] - [HeatlcheckRuleComputation(RuleComputationType.PerDiscover, 10)] - public class HeatlcheckRuleTrustSIDHistoryUnknownDomain : HeatlcheckRuleBase + [RuleModel("T-SIDHistoryUnknownDomain", RiskRuleCategory.Trusts, RiskModelCategory.SIDHistory)] + [RuleComputation(RuleComputationType.PerDiscover, 10)] + [RuleANSSI("R15", "paragraph.3.3.1.5")] + public class HeatlcheckRuleTrustSIDHistoryUnknownDomain : RuleBase { protected override int? AnalyzeDataNew(HealthcheckData healthcheckData) { diff --git a/Healthcheck/Rules/RuleDescription.resx b/Healthcheck/Rules/RuleDescription.resx index f3a290a..0f9bcb8 100644 --- a/Healthcheck/Rules/RuleDescription.resx +++ b/Healthcheck/Rules/RuleDescription.resx @@ -145,7 +145,7 @@ The purpose is to ensure that the minimum set of subnet as been configured in the domain - Locate the IP address which was found as not being part of declared subnet then add this subnet to the "Active Directory Sites" tool. + Locate the IP address which was found as not being part of declared subnet then add this subnet to the "Active Directory Sites" tool. If you found IPv6 addresses and it was not expected, you should disable in addition the protocol IPv6 on the network card. The subnet declaration is incomplete [{count} ip of DC not found in declared subnets] @@ -235,7 +235,8 @@ To solve this security issue, you should change the ownership of the domain controller to match the "Domain Administrators" group. To control the ownership of domain controller objects, you can use the following powershell command: -<i>Get-ADComputer -server my.domain.to.check -LDAPFilter "(&(objectCategory=computer)(|(primarygroupid=521)(primarygroupid=516)))" -properties name, ntsecuritydescriptor | select name,{$_.ntsecuritydescriptor.Owner}</i>. +<i>Get-ADComputer -server my.domain.to.check -LDAPFilter "(&(objectCategory=computer)(|(primarygroupid=521)(primarygroupid=516)))" -properties name, ntsecuritydescriptor | select name,{$_.ntsecuritydescriptor.Owner}</i>. +To change it, you can edit the owner of an object using <a href="https://docs.microsoft.com/en-us/sysinternals/downloads/adexplorer">adexplorer.exe</a>. First, locate the dc object then right click to select properties. Open the security tab and press the advanced button. You have then a new dialog with an owner tab. Select the owner and change it for the domain administrators group. You’re done (no reboot needed) {count} domain controller(s) have been found where the owner is not the Domain admins group or the Enterprise admins group @@ -388,7 +389,7 @@ To control the ownership of domain controller objects, you can use the following The purpose is to ensure that the SID History creation is not enabled - If a migration is in progress, declare it in PingCastle so this rule won't be triggered. Else, remove this auditing group. + If a migration is in progress, declare it in PingCastle so this rule won't be triggered. Else, remove this auditing group. You can locate it by using the LDAP query (sAMAccountNmae=*$$$) The SIDHistory auditing group is present: SID History creation is enabled @@ -623,7 +624,7 @@ There are several possibilities to change the krbtgt password. First, a Microsof The purpose is to ensure that all Administrator Accounts have the configuration flag "this account is sensitive and cannot be delegated" - To correct the situation, you should make sure that all your Administrator Accounts has the checkbox "This account is sensitive and cannot be delegated" active. + To correct the situation, you should make sure that all your Administrator Accounts has the checkbox "This account is sensitive and cannot be delegated" active. Please not that there is a section bellow in this report named "Admin Groups" which give more information. Presence of Admin accounts which have not the flag "this account is sensitive and cannot be delegated": {count} @@ -653,7 +654,16 @@ There are several possibilities to change the krbtgt password. First, a Microsof The purpose is to verify if the Native Administrator account is used. - To mitigate the security risk, a good practice is to use the Native Administrator account only for emergency, while the daily work is performed through other accounts. It is indeed strongly recommended to not use this account but to use nominative account for administrators and dedicated account for services.Do note that the anomaly will be removed 35 days after the last native administrator login. + To mitigate the security risk, a good practice is to use the Native Administrator account only for emergency, while the daily work is performed through other accounts. + It is indeed strongly recommended to not use this account but to use nominative account for administrators and dedicated account for services. + Do note that the anomaly will be removed 35 days after the last native administrator login. + + To track where the administrator account has been used for the last time, we recommend to extract the attribute LastLogon of the administrator account on ALL domain controllers. + It can be done with tools such as ADSIEdit or ADExplorer. + Then, for each domain controller, extract the events 4624 at the date matching the LastLogon date. You will identify the computer and the process at the origin of the logon event. + + Please note that PingCastle relies on the attribute LastLogonTimestamp to perform this check. The LastLogonTimestamp attribute is replicated but has a latency of a maximum of 14 days, while LastLogon is updated at each logon and is more accurate but not replicated. + The native administrator account has been used recently: {count} day(s) ago @@ -762,7 +772,7 @@ There are several possibilities to change the krbtgt password. First, a Microsof A trust without SID Filtering means either that a migration is in progress or that the domain can be compromised instantly via the trust. -The solution is to complete exisring migration ASAP and enable the SID Filtering feature +The solution is to complete existing migration ASAP and enable the SID Filtering feature If the trust is a domain trust, you should use netdom /quarantine and set it to yes If the trust is a forest trust, you should use netdom /enablesidhistory and set it to no @@ -787,7 +797,7 @@ Do not apply /quarantine on a forest trust: you will break the transitivity of t The purpose is to verify if there are accounts currently running with a reversible password - To remove this risk, there should be no account with reversible encryption. You should remove them by removing the flag "Store password using reversible encryption" on all accounts, so that the cleartext password is removed at the next password change. You can get a list of all the possibly compromised accounts running the followinng powershell command: <i>get-adobject -ldapfilter "(userAccountControl:1.2.840.113556.1.4.803:=128)" -properties useraccountcontrol</i> + To remove this risk, there should be no account with reversible encryption. You should remove them by removing the flag "Store password using reversible encryption" on all accounts, so that the cleartext password is removed at the next password change. You can get a list of all the possibly compromised accounts running the following powershell command: <i>get-adobject -ldapfilter "(userAccountControl:1.2.840.113556.1.4.803:=128)" -properties useraccountcontrol</i> Number of computers which have a reversible password: {count} @@ -796,7 +806,7 @@ Do not apply /quarantine on a forest trust: you will break the transitivity of t The purpose is to verify if there are user accounts currently running with a reversible password - To remove this risk, there should be no account with reversible encryption. You should remove them by removing the flag "Store password using reversible encryption" on all accounts, so that the cleartext password is removed at the next password change. You can get a list of all the possibly compromised accounts running the followinng powershell command: <i>get-adobject -ldapfilter "(userAccountControl:1.2.840.113556.1.4.803:=128)" -properties useraccountcontrol</i> + To remove this risk, there should be no account with reversible encryption. You should remove them by removing the flag "Store password using reversible encryption" on all accounts, so that the cleartext password is removed at the next password change. You can get a list of all the possibly compromised accounts running the following powershell command: <i>get-adobject -ldapfilter "(userAccountControl:1.2.840.113556.1.4.803:=128)" -properties useraccountcontrol</i> Number of accounts which have a reversible password: {count} @@ -805,7 +815,7 @@ Do not apply /quarantine on a forest trust: you will break the transitivity of t The purpose is to ensure that there is every account requires a password - The best solution to solve the problem is to change the "useraccountcontrol" attribue of all the accounts that have it and that are not used in trusts. If the flag is removed while there is no password set, you will have an error. You can use this to detect passwords without any accounts. Do note that you can manually check all the accounts that need to be worked on using the following powershell command: <i>get-adobject -ldapfilter "(&(objectCategory=person)(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=32))" -properties useraccountcontrol</i> + The best solution to solve the problem is to change the "useraccountcontrol" attribue of all the accounts that have it and that are not used in trusts. If the flag is removed while there is no password set, you will have an error. You can use this to detect accounts without any passwords. Do note that you can manually check all the accounts that need to be worked on using the following powershell command: <i>get-adobject -ldapfilter "(&(objectCategory=person)(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=32))" -properties useraccountcontrol</i> Number of accounts which can have an empty password (can be overriden by GPO): {count} @@ -826,7 +836,7 @@ Do not apply /quarantine on a forest trust: you will break the transitivity of t Unless stronly justified, change the primary group id to its default. 513 or 514 for users, 516 or 521 for domain controllers, 514 or 515 for computers. The primary group can be edited in a friendly manner by editing the account with the "Active Directory Users and Computers" and after selecting the "Member Of" tab, "set primary group". - Presence of wrong primary group: {count} + Presence of wrong primary group for computers: {count} The purpose is to check for unsual value in the primarygroupid attribute used to store group membership @@ -835,13 +845,13 @@ Do not apply /quarantine on a forest trust: you will break the transitivity of t Unless stronly justified, change the primary group id to its default. 513 or 514 for users, 516 or 521 for domain controllers, 514 or 515 for computers. The primary group can be edited in a friendly manner by editing the account with the "Active Directory Users and Computers" and after selecting the "Member Of" tab, "set primary group". - Presence of wrong primary group: {count} + Presence of wrong primary group for users: {count} The purpose is to ensure that there are as few inactive computers as possible within the domain - To mitigate the risk, you should monitore the number of inactive accounts and reduce it as much as possible. A list of all inactive accounts is obtainable through the command: <i>Search-ADaccount -ComputersOnly -AccountInactive -Timespan 180</i>. + To mitigate the risk, you should monitor the number of inactive accounts and reduce it as much as possible. A list of all inactive accounts is obtainable through the command: <i>Search-ADaccount -ComputersOnly -AccountInactive -Timespan 180</i>. Relatively high number of inactive computer accounts: {count}% (more than {threshold}% of all computers) @@ -905,7 +915,7 @@ Each security descriptor of the domain (including file shares for example) shoul Retrieve data from the domain without any account - Check the procesuss of registration of computers to the domain + Check the processus of registration of computers to the domain Check for presence of the Protected users group @@ -1044,7 +1054,7 @@ It is possible to verify the results provided by the PingCastle solution by usin A verification is done on the backups, ensuring that the backup is performed according to Microsoft standard. Indeed at each backup the DIT Database Partition Backup Signature is updated.  if for any reasons, backups are needed to perform a rollback (rebuild a domain) or to track past changes, the backups will actually be up to date. This check is equivalent to a <i>REPADMIN /showbackup *</i>. - The way an Active Directory behave can be controlled via the attribute <i>DsHeuristics</i> of <i>CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration</i>. A parameter stored in its attribute and whose value is <i>LDAPBlockAnonOps</i> can be set to allow access without any account on the <b>whole forest level</b>. + The way an Active Directory behave can be controlled via the attribute <i>DsHeuristics</i> of <i>CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration</i>. A parameter stored in its attribute and whose value is <i>fLDAPBlockAnonOps</i> can be set to allow access without any account on the <b>whole forest level</b>. It is possible to verify the results provided by the PingCastle solution by using a Kali distribution. You should run <i>rpcclient -U " target_ip_address</i> and press enter at the password prompt to finally type <i>enumdomusers</i>. @@ -1160,7 +1170,7 @@ Do note that the AES key used to encrypt password in the GPO has been made publi By default, a basic user can register up to 10 computers within the domain. This default set up represents a security issue as basic users shouldn't be able to create such accounts, this task being handled by administrators - Inactive computers often stay in the network because of weaknesses in the decomissioning process. These staled computer accounts can be used as backdoors and therefore represents a possible security breach. + Inactive computers often stay in the network because of weaknesses in the decomissioning process. These stale computer accounts can be used as backdoors and therefore represents a possible security breach. In Active Directory, group membersip is stored on the "members" attribute and on the "primarygroupid" attribute. The default primary group value is "Domain Users" for the users, "Domain Computers" for the computers and "Domain Controllers" for the domain controllers. The primarygroupid contains the RID (last digits of a SID) of the group targeted. It can be used to store hidden membership as this attribute is not often analyzed. @@ -1178,7 +1188,7 @@ Do note that the AES key used to encrypt password in the GPO has been made publi Domain Controller needs to be updated regularly because threats to the AD evolve all the time, so assets in the AD should evolve accordingly. The date of last update is computed by getting the <i>StatisticsStartTime</i> from <i>[net statistics workstation]</i>. If not available, the PingCastle solution will use the <i>lastLogonTimestamp</i> attribute which is refreshed based on the LastLogon attribute. Do note that there is a maximum delay for refresh: 14 days. - When multiple sites are created in a domain, networks should be declared in the domain in order to optimize processes such as DC attribution. In addition, PingCastle can collect the information to be able to build a network map. This rule has been triggerd because at least one domain controller has an IP address which was not found in subnet declaration. + When multiple sites are created in a domain, networks should be declared in the domain in order to optimize processes such as DC attribution. In addition, PingCastle can collect the information to be able to build a network map. This rule has been triggered because at least one domain controller has an IP address which was not found in subnet declaration. These IP addresses have been collected by querying the DC FQDN IP address in both IPv6 and IPv4 format. DES is very weak algorithm and once assigned to an account, it can be used to sign Kerberos ticket, even though it is easily breakable. It respresents a security risk for the kerberos ticket, therefore for the whole AD @@ -1190,7 +1200,7 @@ Do note that the AES key used to encrypt password in the GPO has been made publi In order to identify a duplicate account, a check is performed on the "DN" and the "sAMAccountName". Indeed, when a DC detects a conflict, there is a replacement performed on the second object - Inactive accounts often stay in the network because of weaknesses in the decomissioning process. These staled computer accounts can be used as backdoors and therefore represents a possible security breach. + Inactive accounts often stay in the network because of weaknesses in the decomissioning process. These stale computer accounts can be used as backdoors and therefore represents a possible security breach. The Windows 2000 OS is not supported any longer, as it is vulnerable to many publicly known exploits: Administrator's credentials can be captured, security protocols are weak, etc. @@ -1230,7 +1240,7 @@ Do note that the AES key used to encrypt password in the GPO has been made publi A Downlevel trust is a special kind of trust compatible with NT4. The kind of trust can be displayed in the "Active Directory Domains and Trusts" tool. - When a trust is active, it is using a shared secret to communicate to a domain. This secret is hold an a special account whose name is the remote domain name. This password is changed every month and as consequence the whenChanged attribute of this account is changed. When there is no modification of the whenChanged attribute, it can be guessed that the secret has not being changed and that there was either a problem with the remote domain or that the remote domain does not exist anymore. + When a trust is active, it is using a shared secret to communicate to a domain. This secret is hold in a special account whose name is the remote domain name. This password is changed every month and as consequence the whenChanged attribute of this account is changed. When there is no modification of the whenChanged attribute, it can be guessed that the secret has not being changed and that there was either a problem with the remote domain or that the remote domain does not exist anymore. Login script can be stored in any file share available in the network and that includes trusted domains shares. If a login script is located in a compromise domain, it can be used to compromise other domains. @@ -1528,7 +1538,7 @@ https://github.com/misterch0c/shadowbroker/tree/master/windows/exploits Number of accounts which do not require kerberos pre authentication: {count} - Edit the property of the accounts and uncheck "Do not require Kerberos preauthentication" + Edit the property of the involved accounts and select the Account tab. Uncheck "Do not require Kerberos preauthentication". For computers which doesn't have the Account tab, you have to manually edit the attribute useraccountcontrol. Substract from the attribute the value 4194304. Without kerberos preauthentication, an attacker can request kerberos data from the domain controller and use this data to bruteforce the account password. You can search accounts using the ldap query <i>(userAccountControl:1.2.840.113556.1.4.803:=4194304)</i> @@ -1557,4 +1567,398 @@ https://github.com/misterch0c/shadowbroker/tree/master/windows/exploits Script: {0} Account: {1} Right: {2} - \ No newline at end of file + + Ensure that the printer spooler cannot be abused to get the DC Credentials + + + The purpose is to ensure that credentials cannot be extracted from the DC via its printer spooler + + + The spooler service should be deactived on domain controllers. Please note as a consequence that the Printer Pruning functionality (rarely used) will be unavailable. + + + The spooler service is remotely acccessible from {count} DC + + + When there’s an account with unconstrained delegation configured (which is fairly common) and the Print Spooler service running on a computer, you can get that computers credentials sent to the system with unconstrained delegation as a user. With a domain controller, the TGT of the DC can be extracted allowing an attacker to reuse it with a DCSync attack and obtain all user hashes and impersonate them. + + + Domain Controller {0} + + + https://adsecurity.org/?p=4056 +https://www.slideshare.net/harmj0y/derbycon-the-unintended-risks-of-trusting-active-directory + + + Ensure that there are enough DC to provide basic redundancy + + + The purpose is to ensure the failure of one domain controller will not stop the domain. + + + Increase the number of domain controllers by installing new ones. + + + The number of DC is too small to provide redundancy: {count} DC + + + A single domain controller failure can lead to a lack of availability of the domain if the number of servers is too low. To have a minimum redundancy, the number of DC should be at least 2. For Labs, this rule can be ignored and you can add this rule into the exception list. + + + https://social.technet.microsoft.com/wiki/contents/articles/14355.capacity-planning-for-active-directory-domain-services.aspx + + + Ensure that no accounts are subject to unconstrained delegation + + + The purpose is to ensure no account can impersonate any account. + + + Replace unconstrained delegation by contrained delegation. In practice, on the account object, tab "delegation", replace "trust this computer for delegation to any service" by "trust this computer for delegation to specified services only". + + + Unconstrained delegations are configured on the domain: {count} account(s) + + + When an unconstrained delegation is configured, the kerberos ticket TGT can be captured. This TGT grant then access to any service the user has access. If the user is administor or a domain controller (a connection can be forced using the spooler service), the domain can be compromised. + + + https://blogs.technet.microsoft.com/389thoughts/2017/04/18/get-rid-of-accounts-that-use-kerberos-unconstrained-delegation/ +https://adsecurity.org/?p=1667 + + + {0},{1} + + + Ensure that Exchange did not modify the AdminSDHolder object to introduce vulnerabilities + + + The purpose is to ensure that no weakness has been introduced at Exchange installation. + + + After having carefuly studied the possible impact of the following change, alter the AdminSDHolder permissions to remove the Exchange objects. + + + Exchange did alter the AdminSDHolder object + + + At install time, the Exchange Windows Permissions universal security group (USG) was granted the ability to modify the members attribute, the ability to change and reset passwords, and the ability to modify the permissions of any object protected by the AdminSDHolder role. + This security group includes all the Exchange servers. + As a consequence, a malicious administrator could elevate their privileges on one of this server and thus gain control of the Active Directory forest. + Newest versions of Exchange do not introduce this security vulnerability. + + + https://blogs.technet.microsoft.com/exchange/2009/09/23/exchange-2010-and-resolution-of-the-adminsdholder-elevation-issue/ + + + Ensure that boggus Windows 2016 AD prep did not introduce vulnerabilities + + + The purpose is to ensure that no weakness has been introduced at Windows 2016 installation. + + + After having carefuly studied the possible impact of the following change, apply the script made by MSRC and referenced in the documentation below to alter the permission. + + + A bogus Windows 2016 installation has granted to much right to the Enterprise Key Admins group + + + After performing adprep /domainprep from Windows Server 2016 sources there may be an unwanted AccessControlEntry (ACE) in the DiscretionaryACL (DACL) of the targeted domain-naming-context's SecurityDescriptor (SD) that grants FullControl permission to the Enterprise Key Admins group ( SID = ending with -527 ). +This is s a bug in ADPREP that was fixed in Windows Server 2016 RS3/1709. No official fix for those who used pre-1709. +Note: The SID will only be resolvable after the PDC emulator role is transferred to a Windows Server 2016 domain controller. + + + + Found in DN {0} for account {1} and right {2} + + + https://gallery.technet.microsoft.com/scriptcenter/Enterprise-Key-Admins-720eb270 +https://secureidentity.se/adprep-bug-in-windows-server-2016/ + + + The purpose is to ensure that the operator groups which can have indirect control to the domain are empty + + + Group: {0} Member counts: {1} + + + + + + {count} operator group(s) are not empty + + + It is recommended to have these groups empty. Assign administrators into administrators group. Other accounts should have proper delegation rights in OU or in the scope they are managing. + + + Operator groups (account operators, server operators, ...) can take indirectly the control of the domain. Indeed these groups have write access to critical resources of the domain. + + + Check that operators group are empty + + + The purpose is to ensure that standard users cannot modify GPO + + + + + + Number of gpo items that can be modified by any user: {count} + + + Edit the Access Control List (ACL) of the GPO object or the directory where the items is located. Then remove any write permission given to the group. + + + When the group Authenticated Users, Everyone or any similar groups have permission to modify a GPO, it can be abused to take control of the accounts where this GPO applies. It can potentically lead to the compromise of the domain + + + Ensure that GPO items cannot be modified by any user + + + GPO: {0} Item: {1} Account: {2} Right: {3} + + + The purpose is to ensure that standard users are not granted dangerous privileges + + + https://www.romhack.io/slides/RomHack%202018%20-%20Andrea%20Pierini%20-%20whoami%20priv%20-%20show%20me%20your%20Windows%20privileges%20and%20I%20will%20lead%20you%20to%20SYSTEM.pdf +https://www.tarlogic.com/en/blog/abusing-seloaddriverprivilege-for-privilege-escalation/ +https://github.com/decoder-it/psgetsystem + + + Number of privileges granted by GPO to any user: {count} + + + Locate the GPO specified in Details and remove the privilege. +Most of the settings are located in : + Computer configuration -> Policies -> Windows Settings ->Security Settings -> Local Policies -> User Rights Assignment. +As an alternative, the file GptTmpl.inf can be manually edited. + + + To perform special operations, the operating system relies on privileges. They can be displayed by running the command: whoami /all. +SeLoadDriverPrivilege can be used to take control of the system by loading a specifically designed driver. This procedure can be performed by low privileged users as the driver can be defined in HKCU. +SeTcbPrivilege is the privilege used to "Act on behalf the operating system". This is the privilege reserved to the SYSTEM user. This procedure allow any users to act as SYSTEM. +SeDebugPrivilege is the privilege used to debug program and to access any program's memory. It can be used to create a new process and set the parent process to a privileged one. +SeRestorePrivilege can be used to modifiy a service running as local system and startable by all users to a choosen one. +SeBackupPrivilege can be used to backup Windows registry and use third party tools for extracting local NTLM hashes. +SeTakeOwnershipPrivilege can be used to take ownership of any securable object in the system including a service registry key. Then to change its ACL to define its own service running as LocalSystem. +SeCreateTokenPrivilege can be used to create a custom token with all privileges and thus be absued like SeTcbPrivilege +SeImpersonatePrivilege and SeAssignPrimaryTokenPrivilege can be abused to impersonate privileged tokens. These tokens can be retrieved by establishing security context such as Local DCOM DCE/RPC reflexion. + + + Ensure that dangerous privileges are not granted to everyone by GPO + + + GPO: {0} Account: {1} Privilege: {2} + + + The detail can be found in the <a href="#admincountequalsone">AdminSDHolder User List</a> + + + The detail can be found in <a href="#lsasettings">LSA settings</a> + + + The detail can be found in <a href="#backup">Backup</a> + + + The detail can be found in <a href="#certificates">Certificates</a> + + + The detail can be found in <a href="#certificates">Certificates</a> + + + The detail can be found in <a href="#certificates">Certificates</a> + + + The detail can be found in <a href="#certificates">Certificates</a> + + + The detail can be found in <a href="#certificates">Certificates</a> + + + The detail can be found in <a href="#certificates">Certificates</a> + + + The detail can be found in <a href="#certificates">Certificates</a> + + + The detail can be found in <a href="#certificates">Certificates</a> + + + The detail can be found in <a href="#certificates">Certificates</a> + + + The detail can be found in <a href="#certificates">Certificates</a> + + + The detail can be found in <a href="#certificates">Certificates</a> + + + The detail can be found in <a href="#domaincontrollersection">Domain controllers</a> + + + The detail can be found in <a href="#krbtgt">Krbtgt</a> + + + The detail can be found in <a href="#laps">LAPS</a> + + + The detail can be found in <a href="#lsasettings">LSA settings</a> + + + The detail can be found in <a href="#passwordpolicies">Password policies</a> + + + The detail can be found in <a href="#domaincontrollersection">Domain controllers</a> + + + The detail can be found in <a href="#domaincontrollersection">Domain controllers</a> and <a href="#nullsession">Null Session</a> + + + The detail can be found in the <a href="#gpoobfuscatedpassword">Obfuscated Passwords</a> + + + The detail can be found in <a href="#lsasettings">LSA settings</a> + + + The detail can be found in <a href="#lsasettings">LSA settings</a> + + + The schema version is indicated in <a href="#domaininformation">Domain Information</a> + + + The detail can be found in <a href="#passwordpolicies">Password Policies</a> + + + The detail can be found in <a href="#smartcardmandatorywithnopasswordchange">Smart Card and Password</a> + + + The detail can be found in <a href="#domaincontrollersection">Domain controllers</a> + + + The detail can be found in <a href="#domaincontrollersection">Domain controllers</a> + + + The detail can be found in <a href="#admingroups">Admin Groups</a> + + + The detail can be found in <a href="#admindelegation">Delegations</a> + + + The detail can be found in <a href="#domaincontrollersection">Domain controllers</a> + + + The detail can be found in <a href="#admingroups">Admin Groups</a> + + + The detail can be found in <a href="#admindelegation">Delegations</a> + + + The detail can be found in <a href="#admindelegation">Delegations</a> + + + The detail can be found in <a href="#gpologin">GPO Login script</a> + + + The detail can be found in <a href="#admindelegation">Delegations</a> + + + The detail can be found in <a href="#admingroups">Admin Groups</a> + + + The detail can be found in <a href="#gpoprivileges">Privileges</a> + + + The detail can be found in <a href="#admingroups">Admin Groups</a> + + + The detail can be found in <a href="#admingroups">Admin Groups</a> + + + The detail can be found in <a href="#useraccountanalysis">User information</a> and <a href="#computeraccountanalysis">Computer information</a> + + + The detail can be found in <a href="#admindelegation">Delegations</a> + + + The detail can be found in <a href="#domaincontrollersection">Domain controllers</a> + + + The detail can be found in <a href="#domaincontrollersection">Domain controllers</a> + + + The detail can be found in <a href="#useraccountanalysis">User information</a> and <a href="#computeraccountanalysis">Computer information</a> + + + The detail can be found in <a href="#useraccountanalysis">User information</a> and <a href="#computeraccountanalysis">Computer information</a> + + + The detail can be found in <a href="#useraccountanalysis">User information</a> and <a href="#computeraccountanalysis">Computer information</a> + + + The detail can be found in <a href="#useraccountanalysis">User information</a> and <a href="#computeraccountanalysis">Computer information</a> + + + The startup time of domain controllers can be found in <a href="#domaincontrollersection">Domain controllers</a> + + + The startup time of domain controllers can be found in <a href="#domaincontrollersection">Domain controllers</a> + + + The detail can be found in <a href="#operatingsystems">Operating Systems</a> + + + The detail can be found in <a href="#operatingsystems">Operating Systems</a> + + + The operating system of domain controllers can be found in <a href="#domaincontrollersection">Domain controllers</a> + + + The operating system of domain controllers can be found in <a href="#domaincontrollersection">Domain controllers</a> + + + The detail can be found in <a href="#operatingsystems">Operating Systems</a> + + + The detail can be found in <a href="#operatingsystems">Operating Systems</a> + + + The detail can be found in <a href="#useraccountanalysis">User information</a> and <a href="#computeraccountanalysis">Computer information</a> + + + The detail can be found in <a href="#useraccountanalysis">User information</a> and <a href="#computeraccountanalysis">Computer information</a> + + + The detail can be found in <a href="#useraccountanalysis">User information</a> and <a href="#computeraccountanalysis">Computer information</a> + + + The detail can be found in <a href="#useraccountanalysis">User information</a> and <a href="#computeraccountanalysis">Computer information</a> + + + The detail can be found in <a href="#useraccountanalysis">User information</a> and <a href="#computeraccountanalysis">Computer information</a> + + + The SIDHistory detail can be found in <a href="#useraccountanalysis">User information</a> and <a href="#computeraccountanalysis">Computer information</a> and a quick summary in <a href="#sidhistory">SID History</a> + + + The detail can be found in <a href="#domaincontrollersection">Domain controllers</a> + + + The detail can be found in <a href="#discovereddomains">Trusts section</a> + + + The detail can be found in <a href="#discovereddomains">Trusts section</a> + + + The detail can be found in <a href="#gpologin">GPO Login script</a> and in <a href="#logonscripts">Logon Scripts</a> + + + The detail can be found in <a href="#discovereddomains">Trusts section</a> + + + The SIDHistory detail can be found in <a href="#useraccountanalysis">User information</a> and <a href="#computeraccountanalysis">Computer information</a> and a quick summary in <a href="#sidhistory">SID History</a> + + + The SIDHistory detail can be found in <a href="#useraccountanalysis">User information</a> and <a href="#computeraccountanalysis">Computer information</a> and a quick summary in <a href="#sidhistory">SID History</a> + + diff --git a/License.cs b/License.cs index 0837cd7..4310a6b 100644 --- a/License.cs +++ b/License.cs @@ -88,12 +88,12 @@ public ADHealthCheckingLicense(string license) public ADHealthCheckingLicense(string license, bool DoAKeyCheck) { if (String.IsNullOrEmpty(license)) - throw new ApplicationException("No license has been provided"); + throw new PingCastleException("No license has been provided"); _licKey = license; Trace.WriteLine("License: " + _licKey); if (!VerifyKey()) { - throw new ApplicationException("the license couldn't validate"); + throw new PingCastleException("the license couldn't validate"); } } @@ -245,7 +245,7 @@ private void VerifySignature(byte[] signature, byte[] dataToVerify) { Trace.WriteLine("loading rsa key"); Trace.WriteLine("verifying the signature"); - if (!RSA.VerifyHash(hash, "SHA1", signature)) + if (!RSA.VerifyHash(hash, "1.3.14.3.2.26", signature)) { throw new Exception("Invalid signature"); } diff --git a/NativeMethods.cs b/NativeMethods.cs index 3a97b4a..cc35f2b 100644 --- a/NativeMethods.cs +++ b/NativeMethods.cs @@ -78,8 +78,14 @@ public enum SID_NAME_USE SidTypeComputer } + public static string ConvertSIDToName(string sidstring, string server) + { + string referencedDomain = null; + return ConvertSIDToName(sidstring, server, out referencedDomain); + } + [EnvironmentPermissionAttribute(SecurityAction.Demand, Unrestricted = true)] - public static string ConvertSIDToName(string sidstring, string server) + public static string ConvertSIDToName(string sidstring, string server, out string referencedDomain) { StringBuilder name = new StringBuilder(); uint cchName = (uint)name.Capacity; @@ -88,6 +94,7 @@ public static string ConvertSIDToName(string sidstring, string server) SID_NAME_USE sidUse; SecurityIdentifier securityidentifier = null; + referencedDomain = null; try { securityidentifier = new SecurityIdentifier(sidstring); @@ -116,9 +123,15 @@ public static string ConvertSIDToName(string sidstring, string server) err = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); } } - if (err == 0) - return referencedDomainName + "\\" + name; - Trace.WriteLine(@"Error " + err + " when translating " + sidstring + " on " + server); + if (err == 0) + { + referencedDomain = referencedDomainName.ToString(); + if (String.IsNullOrEmpty(referencedDomain)) + return name.ToString(); + else + return referencedDomainName + "\\" + name; + } + Trace.WriteLine(@"Error " + err + " when translating " + sidstring + " on " + server); return sidstring; } @@ -557,5 +570,46 @@ public static DateTime GetStartupTime(string server) NetApiBufferFree(buffer); } } + + [DllImport("winspool.drv", CharSet = CharSet.Unicode, EntryPoint = "OpenPrinterW", SetLastError = true)] + internal static extern bool OpenPrinter(string pPrinterName, out IntPtr phPrinter, IntPtr pDefault); + + [DllImport("winspool.drv", CharSet = CharSet.Unicode, EntryPoint = "ClosePrinter", SetLastError = true)] + internal static extern bool ClosePrinter(IntPtr phPrinter); + + [DllImport("Netapi32.dll", CallingConvention = CallingConvention.Winapi, SetLastError = true, CharSet = CharSet.Auto)] + internal static extern uint DsEnumerateDomainTrusts(string ServerName, + uint Flags, + out IntPtr Domains, + out uint DomainCount); + + [Flags] + internal enum DS_DOMAIN_TRUST_TYPE : uint + { + DS_DOMAIN_IN_FOREST = 0x0001, // Domain is a member of the forest + DS_DOMAIN_DIRECT_OUTBOUND = 0x0002, // Domain is directly trusted + DS_DOMAIN_TREE_ROOT = 0x0004, // Domain is root of a tree in the forest + DS_DOMAIN_PRIMARY = 0x0008, // Domain is the primary domain of queried server + DS_DOMAIN_NATIVE_MODE = 0x0010, // Primary domain is running in native mode + DS_DOMAIN_DIRECT_INBOUND = 0x0020, // Domain is directly trusting + ALL = 0x003F, + } + + [StructLayout(LayoutKind.Sequential)] + internal struct DS_DOMAIN_TRUSTS + { + [MarshalAs(UnmanagedType.LPTStr)] + public string NetbiosDomainName; + [MarshalAs(UnmanagedType.LPTStr)] + public string DnsDomainName; + public uint Flags; + public uint ParentIndex; + public uint TrustType; + public uint TrustAttributes; + public IntPtr DomainSid; + public Guid DomainGuid; + } + } + } diff --git a/PingCastle.csproj b/PingCastle.csproj index d162543..3272485 100644 --- a/PingCastle.csproj +++ b/PingCastle.csproj @@ -86,7 +86,12 @@ - + + + + + + @@ -96,23 +101,63 @@ + + + + + + + + + + + + + + + + + + + + + - - - + + + - + - - - - + + + + + + + + + + + + + + + + - + + + + + + + + @@ -180,11 +225,20 @@ - + - + + + + + + + + + + @@ -196,7 +250,7 @@ - + @@ -206,15 +260,14 @@ - - - - - - - + + + + + + @@ -263,10 +316,22 @@ Designer - + + Designer + + + Designer + + + + + + + + diff --git a/PingCastleException.cs b/PingCastleException.cs new file mode 100644 index 0000000..105e0d0 --- /dev/null +++ b/PingCastleException.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Security.Permissions; +using System.Text; + +namespace PingCastle +{ + [Serializable] + public class PingCastleException : Exception + { + public PingCastleException() + { + } + + public PingCastleException(string message) + : base(message) + { + } + + public PingCastleException(string message, Exception innerException) + : base(message, innerException) + { + } + + protected PingCastleException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/PingCastleFactory.cs b/PingCastleFactory.cs new file mode 100644 index 0000000..60cb312 --- /dev/null +++ b/PingCastleFactory.cs @@ -0,0 +1,95 @@ +using PingCastle.Data; +using PingCastle.Healthcheck; +using PingCastle.Report; +using PingCastle.Reporting; +using PingCastle.Scanners; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Text; + +namespace PingCastle +{ + public class PingCastleFactory + { + public static Dictionary GetAllScanners() + { + var output = new Dictionary(); + foreach (Type type in Assembly.GetAssembly(typeof(PingCastleFactory)).GetExportedTypes()) + { + if (!type.IsAbstract && typeof(IScanner).IsAssignableFrom(type)) + { + PropertyInfo pi = type.GetProperty("Name"); + IScanner scanner = (IScanner)Activator.CreateInstance(type); + output.Add(scanner.Name, type); + } + } + return output; + } + + public static IScanner LoadScanner(Type scannerType) + { + return (IScanner)Activator.CreateInstance(scannerType); + } + + public static string GetFilePatternForLoad() where T : IPingCastleReport + { + if (typeof(T) == typeof(HealthcheckData)) + { + return "*ad_hc_*.xml"; + } + if (typeof(T) == typeof(CompromiseGraphData)) + { + return "*ad_cg_*.xml"; + } + throw new NotImplementedException("No file pattern known for type " + typeof(T)); + } + + public static IPingCastleReportUser GetEndUserReportGenerator() where T : IPingCastleReport + { + if (typeof(T) == typeof(HealthcheckData)) + { + return (IPingCastleReportUser) new ReportHealthCheckSingle(); + } + if (typeof(T) == typeof(CompromiseGraphData)) + { + return (IPingCastleReportUser)new ReportCompromiseGraph(); + } + return GetImplementation>(); + } + + public static IPingCastleAnalyzer GetPingCastleAnalyzer() where T : IPingCastleReport + { + if (typeof(T) == typeof(HealthcheckData)) + { + return (IPingCastleAnalyzer)new HealthcheckAnalyzer(); + } + if (typeof(T) == typeof(CompromiseGraphData)) + { + return (IPingCastleAnalyzer)new ReportGenerator(); + } + return GetImplementation>(); + } + + static T GetImplementation() + { + foreach (Type type in Assembly.GetAssembly(typeof(PingCastleFactory)).GetExportedTypes()) + { + if (typeof(T).IsAssignableFrom(type) && !type.IsAbstract) + { + try + { + return (T)Activator.CreateInstance(type); + } + catch (Exception) + { + Trace.WriteLine("Unable to instanciate the type " + type); + throw; + } + } + } + throw new NotImplementedException("No implementation found for type " + typeof(T).ToString()); + } + } +} diff --git a/Program.cs b/Program.cs index 9ae85a4..c1f82ba 100644 --- a/Program.cs +++ b/Program.cs @@ -5,12 +5,12 @@ // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. // using PingCastle.ADWS; -using PingCastle.Database; +using PingCastle.Graph.Database; using PingCastle.Export; using PingCastle.Healthcheck; using PingCastle.Scanners; using PingCastle.misc; -using PingCastle.NullSession; +using PingCastle.RPC; using PingCastle.Reporting; using PingCastle.shares; using System; @@ -24,55 +24,55 @@ using System.Text; using System.Text.RegularExpressions; using System.Security.Principal; +using PingCastle.Report; +using PingCastle.Data; namespace PingCastle { - [LicenseProvider(typeof(PingCastle.ADHealthCheckingLicenseProvider))] - public class Program : IPingCastleLicenseInfo - { - bool PerformHealthCheckReport = false; - bool PerformHealthCheckConsolidation = false; - bool PerformNullSession = false; - bool PerformNullTrusts = false; - bool PerformGenerateKey = false; - bool PerformCarto = false; - bool PerformAdvancedLive; - bool PerformUploadAllReport; - private bool PerformRegenerateReport; - private bool PerformHealthCheckReloadReport; - bool PerformHealthCheckGenerateDemoReports; - private bool PerformEnumInboundTrust; + [LicenseProvider(typeof(PingCastle.ADHealthCheckingLicenseProvider))] + public class Program : IPingCastleLicenseInfo + { + bool PerformHealthCheckReport = false; + bool PerformHealthCheckConsolidation = false; + bool PerformGraphConsolidation = false; + bool PerformGenerateKey = false; + bool PerformCarto = false; + bool PerformAdvancedLive; + bool PerformUploadAllReport; + private bool PerformRegenerateReport; + private bool PerformHealthCheckReloadReport; + bool PerformHealthCheckGenerateDemoReports; bool PerformScanner = false; - Tasks tasks = new Tasks(); - + Tasks tasks = new Tasks(); - static void Main(string[] args) - { - try - { - AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException); + + public static void Main(string[] args) + { + try + { + AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException); AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve); - Trace.WriteLine("Running on dotnet:" + Environment.Version); - Program program = new Program(); - program.Run(args); - if (program.tasks.InteractiveMode) - { - Console.WriteLine("============================================================================="); - Console.WriteLine("Program launched in interactive mode - press any key to terminate the program"); - Console.WriteLine("============================================================================="); - Console.ReadKey(); - } - } - catch (Exception ex) - { - Tasks.DisplayException("main program", ex); - } - } + Trace.WriteLine("Running on dotnet:" + Environment.Version); + Program program = new Program(); + program.Run(args); + if (program.tasks.InteractiveMode) + { + Console.WriteLine("============================================================================="); + Console.WriteLine("Program launched in interactive mode - press any key to terminate the program"); + Console.WriteLine("============================================================================="); + Console.ReadKey(); + } + } + catch (Exception ex) + { + Tasks.DisplayException("main program", ex); + } + } - static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) - { - Tasks.DisplayException("application domain", e.ExceptionObject as Exception); - } + static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) + { + Tasks.DisplayException("application domain", e.ExceptionObject as Exception); + } static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) { @@ -80,34 +80,33 @@ static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs ar var name = new AssemblyName(args.Name); Trace.WriteLine("Needing assembly " + name + " unknown (" + args.Name + ")"); return null; - //return Assembly.GetExecutingAssembly(); } - private void Run(string[] args) - { - ADHealthCheckingLicense license = null; - Version version = Assembly.GetExecutingAssembly().GetName().Version; - Trace.WriteLine("PingCastle version " + version.ToString(4)); - for(int i = 0; i " + (license.EndTime < DateTime.MaxValue? "End of support: " + license.EndTime.ToShortDateString() : "")); - Console.WriteLine(" | @@@: "); - Console.WriteLine(" : .# Vincent LE TOUX (contact@pingcastle.com)"); - Console.WriteLine(" .: https://www.pingcastle.com"); - if (!ParseCommandLine(args)) - return; - // Trace to file or console may be enabled here + ConsoleMenu.Header = @"|:. PingCastle (Version " + version.ToString(4) + @" " + ConsoleMenu.GetBuildDateTime(Assembly.GetExecutingAssembly()) + @") +| #:. Get Active Directory Security at 80% in 20% of the time +# @@ > " + (license.EndTime < DateTime.MaxValue ? "End of support: " + license.EndTime.ToShortDateString() : "") + @" +| @@@: +: .# Vincent LE TOUX (contact@pingcastle.com) +.: https://www.pingcastle.com"; + if (!ParseCommandLine(args)) + return; + // Trace to file or console may be enabled here Trace.WriteLine("[New run]" + DateTime.Now.ToString("u")); Trace.WriteLine("PingCastle version " + version.ToString(4)); - Trace.WriteLine("Running on dotnet:" + Environment.Version); - if (!String.IsNullOrEmpty(license.DomainLimitation) && !Tasks.compareStringWithWildcard(license.DomainLimitation, tasks.Server)) - { - WriteInRed("Limitations applies to the --server argument (" + license.DomainLimitation + ")"); - return; - } - if (!String.IsNullOrEmpty(license.CustomerNotice)) - { - Console.WriteLine(license.CustomerNotice); - } - if (PerformGenerateKey) - { - if (!tasks.GenerateKeyTask()) return; - } + Trace.WriteLine("Running on dotnet:" + Environment.Version); + if (!String.IsNullOrEmpty(license.DomainLimitation) && !Tasks.compareStringWithWildcard(license.DomainLimitation, tasks.Server)) + { + WriteInRed("Limitations applies to the --server argument (" + license.DomainLimitation + ")"); + return; + } + if (!String.IsNullOrEmpty(license.CustomerNotice)) + { + Console.WriteLine(license.CustomerNotice); + } + if (PerformGenerateKey) + { + if (!tasks.GenerateKeyTask()) return; + } if (PerformScanner) { if (!tasks.ScannerTask()) return; } - if (PerformNullSession) - { - if (!tasks.NullSessionTask()) return; - } - if (PerformNullTrusts) + if (PerformCarto) { - if (!tasks.NullTrustsTask()) return; + if (!tasks.CartoTask(PerformHealthCheckGenerateDemoReports)) return; } - if (PerformEnumInboundTrust) + if (PerformHealthCheckReport) { - if (!tasks.EnumInboundTrustTask()) return; + if (!tasks.AnalysisTask()) return; } - if (PerformCarto) - { - if (!tasks.CartoTask(PerformHealthCheckGenerateDemoReports)) return; - } - if (PerformHealthCheckReport) - { - if (!tasks.HeatlthCheckTask()) return; - } - - if (PerformHealthCheckConsolidation || (tasks.Server == "*" && tasks.InteractiveMode)) - { - if (!tasks.ConsolidationTask()) return; - } - if (PerformRegenerateReport) - { - if (!tasks.RegenerateHtmlTask()) return; - } - if (PerformHealthCheckReloadReport) - { - if (!tasks.ReloadXmlReport()) return; - } - if (PerformHealthCheckGenerateDemoReports && !PerformCarto) - { - if (!tasks.GenerateDemoReportTask()) return; - } - if (PerformAdvancedLive) - { - if (!tasks.AdvancedLiveAnalysisTask()) return; - } - if (PerformUploadAllReport) - { - if (!tasks.UploadAllReportInCurrentDirectory()) return; - } - } + if (PerformAdvancedLive) + { + if (!tasks.AnalysisTask()) return; + } + if (PerformHealthCheckConsolidation || (PerformHealthCheckReport && tasks.Server == "*" && tasks.InteractiveMode)) + { + if (!tasks.ConsolidationTask()) return; + } + if (PerformGraphConsolidation || (PerformAdvancedLive && tasks.Server == "*" && tasks.InteractiveMode)) + { + if (!tasks.ConsolidationTask()) return; + } + if (PerformRegenerateReport) + { + if (!tasks.RegenerateHtmlTask()) return; + } + if (PerformHealthCheckReloadReport) + { + if (!tasks.ReloadXmlReport()) return; + } + if (PerformHealthCheckGenerateDemoReports && !PerformCarto) + { + if (!tasks.GenerateDemoReportTask()) return; + } + if (PerformUploadAllReport) + { + if (!tasks.UploadAllReportInCurrentDirectory()) return; + } + tasks.CompleteTasks(); + } - string _serialNumber; - public string GetSerialNumber() - { - if (String.IsNullOrEmpty(_serialNumber)) - { - try - { - _serialNumber = ADHealthCheckingLicenseSettings.Settings.License; - } - catch (Exception ex) - { - Trace.WriteLine("Exception when getting the license string"); - Trace.WriteLine(ex.Message); - Trace.WriteLine(ex.StackTrace); - throw new ApplicationException("Unable to load the license from the .config file. Check that all files have been copied in the same directory"); - } - } - return _serialNumber; - } + string _serialNumber; + public string GetSerialNumber() + { + if (String.IsNullOrEmpty(_serialNumber)) + { + try + { + _serialNumber = ADHealthCheckingLicenseSettings.Settings.License; + } + catch (Exception ex) + { + Trace.WriteLine("Exception when getting the license string"); + Trace.WriteLine(ex.Message); + Trace.WriteLine(ex.StackTrace); + if (ex.InnerException != null) + { + Trace.WriteLine(ex.InnerException.Message); + Trace.WriteLine(ex.InnerException.StackTrace); + } + throw new PingCastleException("Unable to load the license from the .config file. Check that all files have been copied in the same directory"); + } + } + return _serialNumber; + } - private void WriteInRed(string data) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine(data); - Trace.WriteLine("[Red]" + data); - Console.ResetColor(); - } + private void WriteInRed(string data) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(data); + Trace.WriteLine("[Red]" + data); + Console.ResetColor(); + } - private string GetCurrentDomain() - { - return IPGlobalProperties.GetIPGlobalProperties().DomainName; - } + private string GetCurrentDomain() + { + return IPGlobalProperties.GetIPGlobalProperties().DomainName; + } - // parse command line arguments - private bool ParseCommandLine(string[] args) - { - string user = null; - string userdomain = null; - string password = null; - bool delayedInteractiveMode = false; - if (args.Length == 0) - { - RunInteractiveMode(); - } - else - { - for (int i = 0; i < args.Length; i++) - { - switch (args[i]) - { - case "--adws-port": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --adws-port is mandatory"); - return false; - } - if (!int.TryParse(args[++i], out tasks.ADWSPort)) - { - WriteInRed("argument for --adws-port is not a valid value (typically: 9389)"); - return false; - } - break; - case "--api-endpoint": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --api-endpoint is mandatory"); - return false; - } - tasks.apiEndpoint = args[++i]; - { - Uri res; - if (!Uri.TryCreate(tasks.apiEndpoint, UriKind.Absolute, out res)) - { - WriteInRed("unable to convert api-endpoint into an URI"); - return false; - } - } - break; - case "--api-key": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --api-key is mandatory"); - return false; - } - tasks.apiKey = args[++i]; - break; - case "--auto-reports": - tasks.AutoReport = true; - break; - case "--carto": - PerformCarto = true; - break; - case "--center-on": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --center-on is mandatory"); - return false; - } - tasks.CenterDomainForSimpliedGraph = args[++i]; - break; - case "--debug-license": - break; - case "--demo-reports": - PerformHealthCheckGenerateDemoReports = true; - break; - case "--encrypt": - tasks.EncryptReport = true; - break; - case "--enuminbound": + // parse command line arguments + private bool ParseCommandLine(string[] args) + { + string user = null; + string userdomain = null; + string password = null; + bool delayedInteractiveMode = false; + if (args.Length == 0) + { + if (!RunInteractiveMode()) + return false; + } + else + { + Trace.WriteLine("Before parsing arguments"); + for (int i = 0; i < args.Length; i++) + { + switch (args[i]) + { + case "--api-endpoint": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --api-endpoint is mandatory"); + return false; + } + tasks.apiEndpoint = args[++i]; + { + Uri res; + if (!Uri.TryCreate(tasks.apiEndpoint, UriKind.Absolute, out res)) + { + WriteInRed("unable to convert api-endpoint into an URI"); + return false; + } + } + break; + case "--api-key": if (i + 1 >= args.Length) - { - WriteInRed("argument for --enuminbound is mandatory"); - return false; - } - tasks.EnumInboundSid = args[++i]; - PerformEnumInboundTrust = true; - break; - case "--explore-trust": - tasks.ExploreTerminalDomains = true; - break; - case "--explore-forest-trust": - tasks.ExploreForestTrust = true; - break; - case "--explore-exception": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --explore-exception is mandatory"); - return false; - } - tasks.DomainToNotExplore = new List(args[++i].Split(',')); - break; - case "--filter-date": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --filter-date is mandatory"); - return false; - } - if (!DateTime.TryParse(args[++i], out tasks.FilterReportDate)) - { - WriteInRed("Unable to parse the date \"" + args[i] + "\" - try entering 2016-01-01"); - return false; - } - break; + { + WriteInRed("argument for --api-key is mandatory"); + return false; + } + tasks.apiKey = args[++i]; + break; + case "--carto": + PerformCarto = true; + break; + case "--center-on": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --center-on is mandatory"); + return false; + } + tasks.CenterDomainForSimpliedGraph = args[++i]; + break; + case "--debug-license": + break; + case "--demo-reports": + PerformHealthCheckGenerateDemoReports = true; + break; + case "--encrypt": + tasks.EncryptReport = true; + break; + case "--foreigndomain": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --foreigndomain is mandatory"); + return false; + } + ForeignUsersScanner.EnumInboundSid = args[++i]; + break; + case "--explore-trust": + tasks.ExploreTerminalDomains = true; + break; + case "--explore-forest-trust": + tasks.ExploreForestTrust = true; + break; + case "--explore-exception": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --explore-exception is mandatory"); + return false; + } + tasks.DomainToNotExplore = new List(args[++i].Split(',')); + break; + case "--filter-date": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --filter-date is mandatory"); + return false; + } + if (!DateTime.TryParse(args[++i], out tasks.FilterReportDate)) + { + WriteInRed("Unable to parse the date \"" + args[i] + "\" - try entering 2016-01-01"); + return false; + } + break; case "--regen-report": - PerformRegenerateReport = true; - if (i + 1 >= args.Length) - { + PerformRegenerateReport = true; + if (i + 1 >= args.Length) + { WriteInRed("argument for --regen-report is mandatory"); - return false; - } - tasks.FileOrDirectory = args[++i]; - break; - case "--generate-key": - PerformGenerateKey = true; - break; + return false; + } + tasks.FileOrDirectory = args[++i]; + break; + case "--generate-key": + PerformGenerateKey = true; + break; case "--graph": PerformAdvancedLive = true; break; - case "--healthcheck": - PerformHealthCheckReport = true; - break; - case "--hc-conso": - PerformHealthCheckConsolidation = true; - break; - case "--help": - DisplayHelp(); - return false; - case "--interactive": - delayedInteractiveMode = true; - break; - case "--json-only": - HealthCheckReportMapBuilder.JasonOnly = true; - ReportGenerator.JasonOnly = true; - break; - case "--level": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --level is mandatory"); - return false; - } - try - { - tasks.ExportLevel = (HealthcheckDataLevel)Enum.Parse(typeof(HealthcheckDataLevel), args[++i]); - } - catch (Exception) - { - WriteInRed("Unable to parse the level [" + args[i] + "] to one of the predefined value (" + String.Join(",", Enum.GetNames(typeof(HealthcheckDataLevel))) + ")"); - return false; - } - break; - case "--license": - i++; - break; - case "--log": - EnableLogFile(); - break; - case "--log-console": - EnableLogConsole(); - break; - case "--max-nodes": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --max-nodes is mandatory"); - return false; - } - if (!int.TryParse(args[++i], out tasks.MaxNodes)) - { - WriteInRed("argument for --max-nodes is not a valid value (typically: 1000)"); - return false; - } - break; - case "--max-depth": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --max-depth is mandatory"); - return false; - } - if (!int.TryParse(args[++i], out tasks.MaxDepth)) - { - WriteInRed("argument for --max-depth is not a valid value (typically: 30)"); - return false; - } - break; - case "--no-enum-limit": - HealthCheckReportSingle.MaxNumberUsersInHtmlReport = int.MaxValue; - break; - case "--node": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --node is mandatory"); - return false; - } - tasks.NodesToInvestigate = new List(Regex.Split(args[++i], @"(?= args.Length) - { - WriteInRed("argument for --nodes is mandatory"); - return false; - } - tasks.NodesToInvestigate = new List(File.ReadAllLines(args[++i])); - break; - case "--notifyMail": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --notifyMail is mandatory"); - return false; - } - tasks.mailNotification = args[++i]; - break; - case "--nslimit": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --nslimit is mandatory"); - return false; - } - if (!int.TryParse(args[++i], out tasks.NullSessionEnumerationLimit)) - { - WriteInRed("argument for --nslimit is not a valid value (typically: 5)"); - return false; - } - break; - case "--nullsession": - PerformNullSession = true; - break; - case "--nulltrusts": - PerformNullTrusts = true; + case "--healthcheck": + PerformHealthCheckReport = true; + break; + case "--hc-conso": + PerformHealthCheckConsolidation = true; + break; + case "--cg-conso": + PerformGraphConsolidation = true; + break; + case "--help": + DisplayHelp(); + return false; + case "--interactive": + delayedInteractiveMode = true; + break; + case "--level": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --level is mandatory"); + return false; + } + try + { + tasks.ExportLevel = (PingCastleReportDataExportLevel)Enum.Parse(typeof(PingCastleReportDataExportLevel), args[++i]); + } + catch (Exception) + { + WriteInRed("Unable to parse the level [" + args[i] + "] to one of the predefined value (" + String.Join(",", Enum.GetNames(typeof(PingCastleReportDataExportLevel))) + ")"); + return false; + } + break; + case "--license": + i++; + break; + case "--log": + EnableLogFile(); + break; + case "--log-console": + EnableLogConsole(); + break; + case "--max-nodes": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --max-nodes is mandatory"); + return false; + } + { + int maxNodes; + if (!int.TryParse(args[++i], out maxNodes)) + { + WriteInRed("argument for --max-nodes is not a valid value (typically: 1000)"); + return false; + } + ReportGenerator.MaxNodes = maxNodes; + } + break; + case "--max-depth": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --max-depth is mandatory"); + return false; + } + { + int maxDepth; + if (!int.TryParse(args[++i], out maxDepth)) + { + WriteInRed("argument for --max-depth is not a valid value (typically: 30)"); + return false; + } + ReportGenerator.MaxDepth = maxDepth; + } + break; + case "--no-enum-limit": + ReportHealthCheckSingle.MaxNumberUsersInHtmlReport = int.MaxValue; + break; + case "--node": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --node is mandatory"); + return false; + } + tasks.NodesToInvestigate = new List(Regex.Split(args[++i], @"(?= args.Length) + { + WriteInRed("argument for --nodes is mandatory"); + return false; + } + tasks.NodesToInvestigate = new List(File.ReadAllLines(args[++i])); + break; + case "--notifyMail": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --notifyMail is mandatory"); + return false; + } + tasks.mailNotification = args[++i]; + break; + case "--nslimit": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --nslimit is mandatory"); + return false; + } + if (!int.TryParse(args[++i], out NullSessionScanner.NullSessionEnumerationLimit)) + { + WriteInRed("argument for --nslimit is not a valid value (typically: 5)"); + return false; + } + break; + case "--password": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --password is mandatory"); + return false; + } + password = args[++i]; + break; + case "--port": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --port is mandatory"); + return false; + } + if (!int.TryParse(args[++i], out tasks.Port)) + { + WriteInRed("argument for --port is not a valid value (typically: 9389)"); + return false; + } + break; + case "--protocol": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --protocol is mandatory"); + return false; + } + try + { + ADWebService.ConnectionType = (ADConnectionType)Enum.Parse(typeof(ADConnectionType), args[++i]); + } + catch (Exception ex) + { + Trace.WriteLine(ex.Message); + WriteInRed("Unable to parse the protocol [" + args[i] + "] to one of the predefined value (" + String.Join(",", Enum.GetNames(typeof(ADConnectionType))) + ")"); + return false; + } + break; + case "--reachable": + tasks.AnalyzeReachableDomains = true; break; - case "--password": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --password is mandatory"); - return false; - } - password = args[++i]; - break; - case "--protocol": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --protocol is mandatory"); - return false; - } - try - { - ADWebService.ConnectionType = (ADConnectionType) Enum.Parse(typeof(ADConnectionType), args[++i]); - } - catch (Exception ex) - { - Trace.WriteLine(ex.Message); - WriteInRed("Unable to parse the protocol [" + args[i] + "] to one of the predefined value (" + String.Join(",", Enum.GetNames(typeof(ADConnectionType))) + ")"); - return false; - } - break; - case "--reachable": - tasks.AnalyzeReachableDomains = true; - break; - case "--rev-direction": - tasks.ReverseDirection = true; - break; case "--scanner": if (i + 1 >= args.Length) { @@ -517,559 +509,637 @@ private bool ParseCommandLine(string[] args) return false; } { - var scanners = ScannerBase.GetAllScanners(); + var scanners = PingCastleFactory.GetAllScanners(); string scannername = args[++i]; if (!scanners.ContainsKey(scannername)) { string list = null; - foreach(string name in scanners.Keys) + var allscanners = new List(scanners.Keys); + allscanners.Sort(); + foreach (string name in allscanners) { if (list != null) list += ","; list += name; } - WriteInRed("Unsupported scannername - list is:" + list); + WriteInRed("Unsupported scannername - available scanners are:" + list); } tasks.Scanner = scanners[scannername]; PerformScanner = true; } break; - case "--sendxmlTo": - case "--sendXmlTo": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --sendXmlTo is mandatory"); - return false; - } - tasks.sendXmlTo = args[++i]; - break; - case "--sendhtmlto": - case "--sendHtmlTo": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --sendHtmlTo is mandatory"); - return false; - } - tasks.sendHtmlTo = args[++i]; - break; - case "--sendallto": - case "--sendAllTo": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --sendAllTo is mandatory"); - return false; - } - tasks.sendAllTo = args[++i]; - break; - case "--server": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --server is mandatory"); - return false; - } - tasks.Server = args[++i]; - break; - case "--skip-null-session": - HealthcheckAnalyzer.SkipNullSession = true; - break; - case "--reload-report": - case "--slim-report": - PerformHealthCheckReloadReport = true; - if (i + 1 >= args.Length) - { + case "--scmode-single": + ScannerBase.ScanningMode = 1; + break; + case "--sendxmlTo": + case "--sendXmlTo": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --sendXmlTo is mandatory"); + return false; + } + tasks.sendXmlTo = args[++i]; + break; + case "--sendhtmlto": + case "--sendHtmlTo": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --sendHtmlTo is mandatory"); + return false; + } + tasks.sendHtmlTo = args[++i]; + break; + case "--sendallto": + case "--sendAllTo": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --sendAllTo is mandatory"); + return false; + } + tasks.sendAllTo = args[++i]; + break; + case "--server": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --server is mandatory"); + return false; + } + tasks.Server = args[++i]; + break; + case "--skip-null-session": + HealthcheckAnalyzer.SkipNullSession = true; + break; + case "--reload-report": + case "--slim-report": + PerformHealthCheckReloadReport = true; + if (i + 1 >= args.Length) + { WriteInRed("argument for --slim-report is mandatory"); - return false; - } - tasks.FileOrDirectory = args[++i]; - break; - case "--smtplogin": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --smtplogin is mandatory"); - return false; - } - tasks.smtpLogin = args[++i]; - break; - case "--smtppass": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --smtppass is mandatory"); - return false; - } - tasks.smtpPassword = args[++i]; - break; - case "--smtptls": - tasks.smtpTls = true; - break; - case "--split-ou": - case "--split-OU": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --split-OU is mandatory"); - return false; - } - if (!int.TryParse(args[++i], out tasks.NumberOfDepthForSplit)) - { - WriteInRed("argument for --split-OU is not a valid value (typically: 1,2, ..)"); - return false; - } - break; - case "--upload-all-reports": - PerformUploadAllReport = true; - break; - case "--user": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --user is mandatory"); - return false; - } - i++; - if (args[i].Contains("\\")) - { - int pos = args[i].IndexOf('\\'); - userdomain = args[i].Substring(0, pos); - user = args[i].Substring(pos + 1); - } - else - { - user = args[i]; - } - break; - case "--webdirectory": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --webdirectory is mandatory"); - return false; - } - tasks.sharepointdirectory = args[++i]; - break; - case "--webuser": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --webuser is mandatory"); - return false; - } - tasks.sharepointuser = args[++i]; - break; - case "--webpassword": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --webpassword is mandatory"); - return false; - } - tasks.sharepointpassword = args[++i]; - break; - case "--xmls": - if (i + 1 >= args.Length) - { - WriteInRed("argument for --xmls is mandatory"); - return false; - } - tasks.FileOrDirectory = args[++i]; - break; - default: - WriteInRed("unknow argument: " + args[i]); - DisplayHelp(); - return false; - } - } - } - if (!PerformHealthCheckReport && !PerformHealthCheckConsolidation - && !PerformRegenerateReport && !PerformHealthCheckReloadReport && !delayedInteractiveMode - && !PerformNullSession && !PerformScanner - && !PerformGenerateKey && !PerformHealthCheckGenerateDemoReports && ! PerformCarto && !PerformAdvancedLive - && !PerformNullTrusts && !PerformEnumInboundTrust && !PerformUploadAllReport) - { - WriteInRed("You must choose at least one value among --healthcheck --hc-conso --advanced-export --advanced-report --nullsession --carto"); - DisplayHelp(); - return false; - } - if (delayedInteractiveMode) - { - RunInteractiveMode(); - } - if (PerformHealthCheckReport || PerformScanner || PerformAdvancedLive || PerformNullSession || PerformNullTrusts || PerformEnumInboundTrust) - { - if (String.IsNullOrEmpty(tasks.Server)) - { - tasks.Server = GetCurrentDomain(); - if (String.IsNullOrEmpty(tasks.Server)) - { - WriteInRed("This computer is not connected to a domain. The program couldn't guess the domain or server to connect."); - WriteInRed("Please run again this program with the flag --server or --server "); - DisplayHelp(); - return false; - } - } - if (user != null) - { - if (password == null) - password = AskCredential(); - if (String.IsNullOrEmpty(userdomain)) - { - tasks.Credential = new NetworkCredential(user, password); - } - else - { - tasks.Credential = new NetworkCredential(user, password, userdomain); - } - } - } - if (PerformHealthCheckConsolidation) - { - if (String.IsNullOrEmpty(tasks.FileOrDirectory)) - { - tasks.FileOrDirectory = Directory.GetCurrentDirectory(); - } - else - { - if (!Directory.Exists(tasks.FileOrDirectory)) - { - WriteInRed("The path specified by --xmls isn't a directory"); - DisplayHelp(); - return false; - } - } - } - return true; - } - - private void EnableLogFile() - { - Trace.AutoFlush = true; - TextWriterTraceListener listener = new TextWriterTraceListener("trace.log"); - Trace.Listeners.Add(listener); - } + return false; + } + tasks.FileOrDirectory = args[++i]; + break; + case "--smtplogin": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --smtplogin is mandatory"); + return false; + } + tasks.smtpLogin = args[++i]; + break; + case "--smtppass": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --smtppass is mandatory"); + return false; + } + tasks.smtpPassword = args[++i]; + break; + case "--smtptls": + tasks.smtpTls = true; + break; + case "--upload-all-reports": + PerformUploadAllReport = true; + break; + case "--user": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --user is mandatory"); + return false; + } + i++; + if (args[i].Contains("\\")) + { + int pos = args[i].IndexOf('\\'); + userdomain = args[i].Substring(0, pos); + user = args[i].Substring(pos + 1); + } + else + { + user = args[i]; + } + break; + case "--webdirectory": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --webdirectory is mandatory"); + return false; + } + tasks.sharepointdirectory = args[++i]; + break; + case "--webuser": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --webuser is mandatory"); + return false; + } + tasks.sharepointuser = args[++i]; + break; + case "--webpassword": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --webpassword is mandatory"); + return false; + } + tasks.sharepointpassword = args[++i]; + break; + case "--xmls": + if (i + 1 >= args.Length) + { + WriteInRed("argument for --xmls is mandatory"); + return false; + } + tasks.FileOrDirectory = args[++i]; + break; + default: + WriteInRed("unknow argument: " + args[i]); + DisplayHelp(); + return false; + } + } + Trace.WriteLine("After parsing arguments"); + } + if (!PerformHealthCheckReport && !PerformHealthCheckConsolidation && !PerformGraphConsolidation + && !PerformRegenerateReport && !PerformHealthCheckReloadReport && !delayedInteractiveMode + && !PerformScanner + && !PerformGenerateKey && !PerformHealthCheckGenerateDemoReports && !PerformCarto && !PerformAdvancedLive + && !PerformUploadAllReport) + { + WriteInRed("You must choose at least one value among --healthcheck --hc-conso --advanced-export --advanced-report --nullsession --carto"); + DisplayHelp(); + return false; + } + Trace.WriteLine("Things to do OK"); + if (delayedInteractiveMode) + { + RunInteractiveMode(); + } + if (PerformHealthCheckReport || PerformScanner || PerformAdvancedLive) + { + if (String.IsNullOrEmpty(tasks.Server)) + { + tasks.Server = GetCurrentDomain(); + if (String.IsNullOrEmpty(tasks.Server)) + { + WriteInRed("This computer is not connected to a domain. The program couldn't guess the domain or server to connect."); + WriteInRed("Please run again this program with the flag --server or --server "); + DisplayHelp(); + return false; + } + } + if (user != null) + { + if (password == null) + password = AskCredential(); + if (String.IsNullOrEmpty(userdomain)) + { + tasks.Credential = new NetworkCredential(user, password); + } + else + { + tasks.Credential = new NetworkCredential(user, password, userdomain); + } + } + } + if (PerformHealthCheckConsolidation || PerformGraphConsolidation) + { + if (String.IsNullOrEmpty(tasks.FileOrDirectory)) + { + tasks.FileOrDirectory = Directory.GetCurrentDirectory(); + } + else + { + if (!Directory.Exists(tasks.FileOrDirectory)) + { + WriteInRed("The path specified by --xmls isn't a directory"); + DisplayHelp(); + return false; + } + } + } + return true; + } - private void EnableLogConsole() - { - Trace.AutoFlush = true; - ConsoleTraceListener listener = new ConsoleTraceListener(); - Trace.Listeners.Add(listener); - } + private void EnableLogFile() + { + Trace.AutoFlush = true; + TextWriterTraceListener listener = new TextWriterTraceListener("trace.log"); + Trace.Listeners.Add(listener); + } - private string AskCredential() - { - StringBuilder builder = new StringBuilder(); - Console.WriteLine("Enter password: "); - ConsoleKeyInfo nextKey = Console.ReadKey(true); + private void EnableLogConsole() + { + Trace.AutoFlush = true; + TextWriterTraceListener listener = new TextWriterTraceListener(Console.Out); + Trace.Listeners.Add(listener); + } - while (nextKey.Key != ConsoleKey.Enter) - { - if (nextKey.Key == ConsoleKey.Backspace) - { - if (builder.Length > 0) - { - builder.Remove(builder.Length -1, 1); - // erase the last * as well - Console.Write(nextKey.KeyChar); - Console.Write(" "); - Console.Write(nextKey.KeyChar); - } - } - else - { - builder.Append(nextKey.KeyChar); - Console.Write("*"); - } - nextKey = Console.ReadKey(true); - } - Console.WriteLine(); - return builder.ToString(); - } + private string AskCredential() + { + StringBuilder builder = new StringBuilder(); + Console.WriteLine("Enter password: "); + ConsoleKeyInfo nextKey = Console.ReadKey(true); - // interactive interface - private void RunInteractiveMode() - { - tasks.InteractiveMode = true; - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("Using interactive mode."); - Console.WriteLine("Do not forget that there are other command line switches like --help that you can use"); - Console.ResetColor(); - string whattodo = null; - List> choices = new List>() { - new KeyValuePair("healthcheck","Score the risk of a domain"), - new KeyValuePair("graph","Analyze admin groups and delegations"), - new KeyValuePair("conso","Aggregate multiple reports into a single one"), - new KeyValuePair("nullsession","Perform a specific security check"), - new KeyValuePair("carto","Build a map of all interconnected domains"), - new KeyValuePair("scanner","Perform specific security checks on workstations"), - }; - Console.WriteLine("What you would like to do?"); - whattodo = choices[SelectMenu(choices, 0)].Key; - switch (whattodo) - { - case "": - case "healthcheck": - PerformHealthCheckReport = true; - break; - case "graph": - PerformAdvancedLive = true; - break; - case "carto": - PerformCarto = true; - break; - case "conso": - PerformHealthCheckConsolidation = true; - break; - case "nullsession": - PerformNullSession = true; - break; - case "scanner": - PerformScanner = true; - break; - } - if (PerformScanner) + while (nextKey.Key != ConsoleKey.Enter) { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine("WARNING"); - Console.WriteLine("Checking a lot of workstation in a short time using tcp/445 can raise alerts to a SOC. Be sure to have warned your security team."); - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("Select a scanner"); - Console.WriteLine("============================="); - Console.ResetColor(); - whattodo = null; - var scanners = ScannerBase.GetAllScanners(); - Console.WriteLine("What scanner whould you like to run ?"); - - choices = new List>(); - foreach(var scanner in scanners) + if (nextKey.Key == ConsoleKey.Backspace) { - Type scannerType = scanner.Value; - IScanner iscanner = (IScanner)Activator.CreateInstance(scannerType); - string description = iscanner.Description; - choices.Add(new KeyValuePair(scanner.Key, description)); + if (builder.Length > 0) + { + builder.Remove(builder.Length - 1, 1); + // erase the last * as well + Console.Write(nextKey.KeyChar); + Console.Write(" "); + Console.Write(nextKey.KeyChar); + } + } + else + { + builder.Append(nextKey.KeyChar); + Console.Write("*"); } + nextKey = Console.ReadKey(true); + } + Console.WriteLine(); + return builder.ToString(); + } + + private enum DisplayState + { + Exit, + MainMenu, + ScannerMenu, + AskForServer, + Run, + AvancedMenu, + AskForAdditionalUsers, + AskForScannerParameter, + ProtocolMenu, + AskForFile, + } + + DisplayState DisplayMainMenu() + { + PerformHealthCheckReport = false; + PerformGraphConsolidation = false; + PerformAdvancedLive = false; + PerformCarto = false; + PerformHealthCheckConsolidation = false; + PerformScanner = false; + + List> choices = new List>() { + new KeyValuePair("healthcheck","Score the risk of a domain"), + new KeyValuePair("graph","Analyze admin groups and delegations with diagrams"), + new KeyValuePair("conso","Aggregate multiple reports into a single one"), + new KeyValuePair("carto","Build a map of all interconnected domains"), + new KeyValuePair("scanner","Perform specific security checks on workstations"), + new KeyValuePair("advanced","Open the advanced menu"), + }; + + ConsoleMenu.Title = "What do you want to do?"; + ConsoleMenu.Information = "Using interactive mode.\r\nDo not forget that there are other command line switches like --help that you can use"; + int choice = ConsoleMenu.SelectMenu(choices); + if (choice == 0) + return DisplayState.Exit; - tasks.Scanner = scanners[choices[SelectMenu(choices)].Key]; + string whattodo = choices[choice - 1].Key; + switch (whattodo) + { + default: + case "healthcheck": + PerformHealthCheckReport = true; + return DisplayState.AskForServer; + case "graph": + PerformAdvancedLive = true; + return DisplayState.AskForServer; + case "carto": + PerformCarto = true; + return DisplayState.AskForServer; + case "conso": + PerformHealthCheckConsolidation = true; + PerformGraphConsolidation = true; + return DisplayState.Run; + case "scanner": + PerformScanner = true; + return DisplayState.ScannerMenu; + case "advanced": + return DisplayState.AvancedMenu; } - if (PerformHealthCheckReport || PerformNullSession || PerformScanner || PerformAdvancedLive || PerformEnumInboundTrust) - { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("Select a domain or server"); - Console.WriteLine("============================="); - Console.ResetColor(); - string defaultDomain = tasks.Server; - if (String.IsNullOrEmpty(defaultDomain)) - defaultDomain = GetCurrentDomain(); - while (true) - { - if (!String.IsNullOrEmpty(defaultDomain)) - { - Console.WriteLine("Please specify the domain or server to investigate (default:" + defaultDomain + ")"); - } - else - { - Console.WriteLine("Please specify the domain or server to investigate:"); - } - tasks.Server = Console.ReadLine(); - if (String.IsNullOrEmpty(tasks.Server)) - { - tasks.Server = defaultDomain; - } - if (!String.IsNullOrEmpty(tasks.Server)) - { - break; - } - } - } - if (PerformAdvancedLive) - { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine("Indicate additional users"); - Console.WriteLine("============================="); - Console.ResetColor(); - Console.WriteLine("Please specify any additional users to investigate (sAMAccountName, display name) and end by an empty line"); - tasks.NodesToInvestigate = new List(); - whattodo = Console.ReadLine(); - while(!String.IsNullOrEmpty(whattodo)) - { - tasks.NodesToInvestigate.Add(whattodo); - whattodo = Console.ReadLine(); - } - - } - } + } - static void printSelectMenu(List> items, int defaultIndex, int top, int left) + DisplayState DisplayScannerMenu() { - bool hasDescription = false; - int largerChoice = 0; - for (int i = 0; i < items.Count; i++) + var scanners = PingCastleFactory.GetAllScanners(); + + var choices = new List>(); + foreach (var scanner in scanners) { - if (!String.IsNullOrEmpty(items[i].Value)) - hasDescription = true; - int l = items[i].Key.Length; - if (l > largerChoice) - largerChoice = l; + Type scannerType = scanner.Value; + IScanner iscanner = PingCastleFactory.LoadScanner(scannerType); + string description = iscanner.Description; + choices.Add(new KeyValuePair(scanner.Key, description)); } - Console.SetCursorPosition(left, top); - for (int i = 0; i < items.Count; i++) + choices.Sort((KeyValuePair a, KeyValuePair b) + => + { + return String.Compare(a.Key, b.Key); + } + ); + ConsoleMenu.Notice = "WARNING: Checking a lot of workstations may raise security alerts."; + ConsoleMenu.Title = "Select a scanner"; + ConsoleMenu.Information = "What scanner whould you like to run ?"; + int choice = ConsoleMenu.SelectMenuCompact(choices, 1); + if (choice == 0) + return DisplayState.Exit; + tasks.Scanner = scanners[choices[choice - 1].Key]; + return DisplayState.AskForScannerParameter; + } + + DisplayState DisplayAskForScannerParameter() + { + IScanner iscannerAddParam = PingCastleFactory.LoadScanner(tasks.Scanner); + if (!iscannerAddParam.QueryForAdditionalParameterInInteractiveMode()) + return DisplayState.Exit; + return DisplayState.AskForServer; + } + + DisplayState DisplayAskServer() + { + string defaultDomain = tasks.Server; + if (String.IsNullOrEmpty(defaultDomain)) + defaultDomain = GetCurrentDomain(); + while (true) { - if (i == defaultIndex) + if (!String.IsNullOrEmpty(defaultDomain)) { - Console.BackgroundColor = ConsoleColor.Gray; - Console.ForegroundColor = ConsoleColor.Black; + ConsoleMenu.Information = "Please specify the domain or server to investigate (default:" + defaultDomain + ")"; } - Console.Write(" " + (i + 1) + "-" + items[i].Key); - if (hasDescription) + else { - int diff = largerChoice - items[i].Key.Length; - if (diff > 0) - Console.Write(new String(' ', diff)); - if (!String.IsNullOrEmpty(items[i].Value)) - Console.Write("-" + items[i].Value); + ConsoleMenu.Information = "Please specify the domain or server to investigate:"; + } + ConsoleMenu.Title = "Select a domain or server"; + tasks.Server = ConsoleMenu.AskForString(); + if (String.IsNullOrEmpty(tasks.Server)) + { + tasks.Server = defaultDomain; + } + if (!String.IsNullOrEmpty(tasks.Server)) + { + break; } - Console.WriteLine(); - Console.ResetColor(); } + if (PerformAdvancedLive) + { + return DisplayState.AskForAdditionalUsers; + } + return DisplayState.Run; } - public static int SelectMenu(List> items, int defaultIndex = 0) + DisplayState DisplayAskForAdditionalUsers() { - int top = Console.CursorTop; - int left = Console.CursorLeft; - int index = defaultIndex; - Console.CursorVisible = false; - while (true) + ConsoleMenu.Title = "Indicate additional users"; + ConsoleMenu.Information = "Please specify any additional users to investigate (sAMAccountName, display name) in addition to the classic admin groups. One entry per line. End by an empty line."; + tasks.NodesToInvestigate = ConsoleMenu.AskForListString(); + return DisplayState.Run; + } + + DisplayState DisplayAdvancedMenu() + { + PerformGenerateKey = false; + PerformHealthCheckReloadReport = false; + PerformRegenerateReport = false; + + List> choices = new List>() { + new KeyValuePair("protocol","Change the protocol used to query the AD (LDAP, ADWS, ...)"), + new KeyValuePair("generatekey","Generate RSA keys used to encrypt and decrypt reports"), + new KeyValuePair("decrypt","Decrypt a xml report"), + new KeyValuePair("regenerate","Regenerate the html report based on the xml report"), + new KeyValuePair("log","Enable logging (log is " + (Trace.Listeners.Count > 1 ? "enabled":"disabled") + ")"), + }; + + ConsoleMenu.Title = "What do you want to do?"; + int choice = ConsoleMenu.SelectMenu(choices); + if (choice == 0) + return DisplayState.Exit; + + string whattodo = choices[choice - 1].Key; + switch (whattodo) { - printSelectMenu(items, index, top, left); - ConsoleKeyInfo ckey = Console.ReadKey(true); + default: + case "protocol": + return DisplayState.ProtocolMenu; + case "generatekey": + PerformGenerateKey = true; + return DisplayState.Run; + case "decrypt": + PerformHealthCheckReloadReport = true; + return DisplayState.AskForFile; + case "regenerate": + PerformRegenerateReport = true; + return DisplayState.AskForFile; + case "log": + if (Trace.Listeners.Count <= 1) + EnableLogFile(); + return DisplayState.Exit; + } + } - if (ckey.Key == ConsoleKey.DownArrow) - { - if (index == items.Count - 1) - { - //index = 0; //Remove the comment to return to the topmost item in the list - } - else { index++; } - } - else if (ckey.Key == ConsoleKey.UpArrow) + DisplayState DisplayProtocolMenu() + { + List> choices = new List>() { + new KeyValuePair("ADWSThenLDAP","default: ADWS then if failed, LDAP"), + new KeyValuePair("ADWSOnly","use only ADWS"), + new KeyValuePair("LDAPOnly","use only LDAP"), + new KeyValuePair("LDAPThenADWS","LDAP then if failed, ADWS"), + }; + + ConsoleMenu.Title = "What protocol do you want to use?"; + ConsoleMenu.Information = "ADWS (Active Directory Web Service - tcp/9389) is the fastest protocol but is limited 5 sessions in parallele and a 30 minutes windows. LDAP is more stable but slower.\r\nCurrent protocol: [" + ADWebService.ConnectionType + "]"; + int defaultChoice = 1; + for (int i = 0; i < choices.Count; i++) + { + if (choices[i].Key == ADWebService.ConnectionType.ToString()) + defaultChoice = 1 + i; + } + int choice = ConsoleMenu.SelectMenu(choices, defaultChoice); + if (choice == 0) + return DisplayState.Exit; + + string whattodo = choices[choice - 1].Key; + ADWebService.ConnectionType = (ADConnectionType)Enum.Parse(typeof(ADConnectionType), whattodo); + return DisplayState.Exit; + } + + DisplayState DisplayAskForFile() + { + string file = null; + while (String.IsNullOrEmpty(file) || !File.Exists(file)) + { + ConsoleMenu.Title = "Select an existing report"; + ConsoleMenu.Information = "Please specify the report to open."; + file = ConsoleMenu.AskForString(); + ConsoleMenu.Notice = "The file " + file + " was not found"; + } + tasks.FileOrDirectory = file; + tasks.EncryptReport = false; + return DisplayState.Run; + } + + // interactive interface + private bool RunInteractiveMode() + { + tasks.InteractiveMode = true; + Stack states = new Stack(); + var state = DisplayState.MainMenu; + + states.Push(state); + while (states.Count > 0 && states.Peek() != DisplayState.Run) + { + switch (state) { - if (index <= 0) - { - //index = menuItem.Count - 1; //Remove the comment to return to the item in the bottom of the list - } - else { index--; } + case DisplayState.MainMenu: + state = DisplayMainMenu(); + break; + case DisplayState.ScannerMenu: + state = DisplayScannerMenu(); + break; + case DisplayState.AskForServer: + state = DisplayAskServer(); + break; + case DisplayState.AskForAdditionalUsers: + state = DisplayAskForAdditionalUsers(); + break; + case DisplayState.AskForScannerParameter: + state = DisplayAskForScannerParameter(); + break; + case DisplayState.AvancedMenu: + state = DisplayAdvancedMenu(); + break; + case DisplayState.AskForFile: + state = DisplayAskForFile(); + break; + case DisplayState.ProtocolMenu: + state = DisplayProtocolMenu(); + break; + default: + // defensive programming + if (state != DisplayState.Exit) + { + Console.WriteLine("No implementation of state " + state); + state = DisplayState.Exit; + } + break; } - else if (ckey.Key == ConsoleKey.Enter) + if (state == DisplayState.Exit) { - Console.CursorVisible = true; - Console.ResetColor(); - return index; + states.Pop(); + if (states.Count > 0) + state = states.Peek(); } else { - int number; - if (Int32.TryParse(ckey.KeyChar.ToString(), out number) && number > 0 && number <= 9 && (number <= items.Count)) - { - Console.CursorVisible = true; - Console.ResetColor(); - return number-1; - } + states.Push(state); } } + return (states.Count > 0); } - private static void DisplayHelp() - { - Console.WriteLine("switch:"); - Console.WriteLine(" --help : display this message"); - Console.WriteLine(" --interactive : force the interactive mode"); - Console.WriteLine(" --log : generate a log file"); - Console.WriteLine(" --log-console : add log to the console"); - Console.WriteLine(""); - Console.WriteLine("Common options when connecting to the AD"); - Console.WriteLine(" --server : use this server (default: current domain controller)"); - Console.WriteLine(" the special value * or *.forest do the healthcheck for all domains"); - Console.WriteLine(" --adws-port : use the port for ADWS (default: 9389)"); - Console.WriteLine(" --user : use this user (default: integrated authentication)"); - Console.WriteLine(" --password : use this password (default: asked on a secure prompt)"); - Console.WriteLine(" --protocol : selection the protocol to use among LDAP or ADWS (fastest)"); - Console.WriteLine(" : ADWSThenLDAP (default), ADWSOnly, LDAPOnly, LDAPThenADWS"); - Console.WriteLine(""); - Console.WriteLine(" --carto : perform a quick cartography with domains surrounding"); - Console.WriteLine(""); - Console.WriteLine(" --healthcheck : perform the healthcheck (step1)"); - Console.WriteLine(" --api-endpoint <> : upload report via api call eg: http://server"); - Console.WriteLine(" --api-key : and using the api key as registered"); - Console.WriteLine(" --explore-trust : on domains of a forest, after the healthcheck, do the hc on all trusted domains except domains of the forest and forest trusts"); - Console.WriteLine(" --explore-forest-trust : on root domain of a forest, after the healthcheck, do the hc on all forest trusts discovered"); - Console.WriteLine(" --explore-trust and --explore-forest-trust can be run together"); - Console.WriteLine(" --explore-exception : comma separated values of domains that will not be explored automatically"); - Console.WriteLine(""); - Console.WriteLine(" --encrypt : use an RSA key stored in the .config file to crypt the content of the xml report"); - Console.WriteLine(" --level : specify the amount of data found in the xml file"); - Console.WriteLine(" : level: Full, Normal, Light"); - Console.WriteLine(" --no-enum-limit : remove the max 100 users limitation in html report"); - Console.WriteLine(" --reachable : add reachable domains to the list of discovered domains"); - Console.WriteLine(" --split-OU : this is used to bypass the 30 minutes limit per ADWS request. Try 5 and increase 1 by 1."); - Console.WriteLine(" --sendXmlTo : send xml reports to a mailbox (comma separated email)"); - Console.WriteLine(" --sendHtmlTo : send html reports to a mailbox"); - Console.WriteLine(" --sendAllTo : send html reports to a mailbox"); - Console.WriteLine(" --notifyMail : add email notification when the mail is received"); - Console.WriteLine(" --smtplogin : allow smtp credentials ..."); - Console.WriteLine(" --smtppass : ... to be entered on the command line"); - Console.WriteLine(" --smtptls : enable TLS/SSL in SMTP if used on other port than 465 and 587"); - Console.WriteLine(" --skip-null-session: do not test for null session"); - Console.WriteLine(" --webdirectory : upload the xml report to a webdav server"); - Console.WriteLine(" --webuser : optional user and password"); - Console.WriteLine(" --webpassword "); - Console.WriteLine(""); - Console.WriteLine(" --generate-key : generate and display a new RSA key for encryption"); - Console.WriteLine(""); - Console.WriteLine(" --hc-conso : consolidate multiple healthcheck xml reports (step2)"); - Console.WriteLine(" --center-on : center the simplified graph on this domain"); - Console.WriteLine(" default is the domain with the most links"); - Console.WriteLine(" --xmls : specify the path containing xml (default: current directory)"); - Console.WriteLine(" --filter-date : filter report generated after the date."); - Console.WriteLine(""); - Console.WriteLine(" --regen-report : regenerate a html report based on a xml report"); - Console.WriteLine(" --reload-report : regenerate a xml report based on a xml report"); - Console.WriteLine(" any healthcheck switches (send email, ..) can be reused"); - Console.WriteLine(" --level : specify the amount of data found in the xml file"); - Console.WriteLine(" : level: Full, Normal, Light (default: Normal)"); - Console.WriteLine(" --encrypt : use an RSA key stored in the .config file to crypt the content of the xml report"); - Console.WriteLine(" the absence of this switch on an encrypted report will produce a decrypted report"); - Console.WriteLine(""); - Console.WriteLine(" --graph : perform the light compromise graph computation directly to the AD"); + private static void DisplayHelp() + { + Console.WriteLine("switch:"); + Console.WriteLine(" --help : display this message"); + Console.WriteLine(" --interactive : force the interactive mode"); + Console.WriteLine(" --log : generate a log file"); + Console.WriteLine(" --log-console : add log to the console"); + Console.WriteLine(""); + Console.WriteLine("Common options when connecting to the AD"); + Console.WriteLine(" --server : use this server (default: current domain controller)"); + Console.WriteLine(" the special value * or *.forest do the healthcheck for all domains"); + Console.WriteLine(" --port : the port to use for ADWS or LDPA (default: 9389 or 389)"); + Console.WriteLine(" --user : use this user (default: integrated authentication)"); + Console.WriteLine(" --password : use this password (default: asked on a secure prompt)"); + Console.WriteLine(" --protocol : selection the protocol to use among LDAP or ADWS (fastest)"); + Console.WriteLine(" : ADWSThenLDAP (default), ADWSOnly, LDAPOnly, LDAPThenADWS"); + Console.WriteLine(""); + Console.WriteLine(" --carto : perform a quick cartography with domains surrounding"); + Console.WriteLine(""); + Console.WriteLine(" --healthcheck : perform the healthcheck (step1)"); + Console.WriteLine(" --api-endpoint <> : upload report via api call eg: http://server"); + Console.WriteLine(" --api-key : and using the api key as registered"); + Console.WriteLine(" --explore-trust : on domains of a forest, after the healthcheck, do the hc on all trusted domains except domains of the forest and forest trusts"); + Console.WriteLine(" --explore-forest-trust : on root domain of a forest, after the healthcheck, do the hc on all forest trusts discovered"); + Console.WriteLine(" --explore-trust and --explore-forest-trust can be run together"); + Console.WriteLine(" --explore-exception : comma separated values of domains that will not be explored automatically"); + Console.WriteLine(""); + Console.WriteLine(" --encrypt : use an RSA key stored in the .config file to crypt the content of the xml report"); + Console.WriteLine(" --level : specify the amount of data found in the xml file"); + Console.WriteLine(" : level: Full, Normal, Light"); + Console.WriteLine(" --no-enum-limit : remove the max 100 users limitation in html report"); + Console.WriteLine(" --reachable : add reachable domains to the list of discovered domains"); + Console.WriteLine(" --sendXmlTo : send xml reports to a mailbox (comma separated email)"); + Console.WriteLine(" --sendHtmlTo : send html reports to a mailbox"); + Console.WriteLine(" --sendAllTo : send html reports to a mailbox"); + Console.WriteLine(" --notifyMail : add email notification when the mail is received"); + Console.WriteLine(" --smtplogin : allow smtp credentials ..."); + Console.WriteLine(" --smtppass : ... to be entered on the command line"); + Console.WriteLine(" --smtptls : enable TLS/SSL in SMTP if used on other port than 465 and 587"); + Console.WriteLine(" --skip-null-session: do not test for null session"); + Console.WriteLine(" --webdirectory : upload the xml report to a webdav server"); + Console.WriteLine(" --webuser : optional user and password"); + Console.WriteLine(" --webpassword "); + Console.WriteLine(""); + Console.WriteLine(" --generate-key : generate and display a new RSA key for encryption"); + Console.WriteLine(""); + Console.WriteLine(" --hc-conso : consolidate multiple healthcheck xml reports (step2)"); + Console.WriteLine(" --center-on : center the simplified graph on this domain"); + Console.WriteLine(" default is the domain with the most links"); + Console.WriteLine(" --xmls : specify the path containing xml (default: current directory)"); + Console.WriteLine(" --filter-date : filter report generated after the date."); + Console.WriteLine(""); + Console.WriteLine(" --regen-report : regenerate a html report based on a xml report"); + Console.WriteLine(" --reload-report : regenerate a xml report based on a xml report"); + Console.WriteLine(" any healthcheck switches (send email, ..) can be reused"); + Console.WriteLine(" --level : specify the amount of data found in the xml file"); + Console.WriteLine(" : level: Full, Normal, Light (default: Normal)"); + Console.WriteLine(" --encrypt : use an RSA key stored in the .config file to crypt the content of the xml report"); + Console.WriteLine(" the absence of this switch on an encrypted report will produce a decrypted report"); + Console.WriteLine(""); + Console.WriteLine(" --graph : perform the light compromise graph computation directly to the AD"); Console.WriteLine(" --encrypt : use an RSA key stored in the .config file to crypt the content of the xml report"); - Console.WriteLine(" --max-depth : maximum number of relation to explore (default:30)"); - Console.WriteLine(" --max-nodes : maximum number of node to include (default:1000)"); - Console.WriteLine(" --node : create a report based on a object"); - Console.WriteLine(" : example: \"cn=name\" or \"name\""); - Console.WriteLine(" --nodes : create x report based on the nodes listed on a file"); - Console.WriteLine(""); - Console.WriteLine(" --nullsession : test for null session"); - Console.WriteLine(" --nslimit : Limit the number of users to enumerate (default: 5)"); - Console.WriteLine(""); - Console.WriteLine(" --scanner : perform a scan on all computers of the domain (using --server)"); - var scanner = ScannerBase.GetAllScanners(); + Console.WriteLine(" --max-depth : maximum number of relation to explore (default:30)"); + Console.WriteLine(" --max-nodes : maximum number of node to include (default:1000)"); + Console.WriteLine(" --node : create a report based on a object"); + Console.WriteLine(" : example: \"cn=name\" or \"name\""); + Console.WriteLine(" --nodes : create x report based on the nodes listed on a file"); + Console.WriteLine(""); + Console.WriteLine(" --scanner : perform a scan on one of all computers of the domain (using --server)"); + var scanner = PingCastleFactory.GetAllScanners(); var scannerNames = new List(scanner.Keys); scannerNames.Sort(); foreach (var scannerName in scannerNames) { Type scannerType = scanner[scannerName]; - IScanner iscanner = (IScanner)Activator.CreateInstance(scannerType); + IScanner iscanner = PingCastleFactory.LoadScanner(scannerType); Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine(iscanner.Name); Console.ResetColor(); Console.WriteLine(iscanner.Description); } - Console.WriteLine(""); - Console.WriteLine(" --nulltrusts : check if the trusts can be enumerated using null session"); - Console.WriteLine(""); - Console.WriteLine(" --enuminbound : Enumerate accounts from inbound trust using its FQDN or sids"); + Console.WriteLine(" options for scanners:"); + Console.WriteLine(" --scmode-single : force scanner to check one single computer"); + Console.WriteLine(" --nslimit : Limit the number of users to enumerate (default: 5)"); + Console.WriteLine(" --foreigndomain : foreign domain targeted using its FQDN or sids"); Console.WriteLine(" Example of SID: S-1-5-21-4005144719-3948538632-2546531719"); Console.WriteLine(""); - Console.WriteLine(" --upload-all-reports: use the API to upload all reports in the current directory"); - Console.WriteLine(" --api-endpoint <> : upload report via api call eg: http://server"); - Console.WriteLine(" --api-key : and using the api key as registered"); - Console.WriteLine(" Note: do not forget to set --level Full to send all the information available"); - Console.WriteLine(""); + Console.WriteLine(" --upload-all-reports: use the API to upload all reports in the current directory"); + Console.WriteLine(" --api-endpoint <> : upload report via api call eg: http://server"); + Console.WriteLine(" --api-key : and using the api key as registered"); + Console.WriteLine(" Note: do not forget to set --level Full to send all the information available"); + Console.WriteLine(""); - } - } + } + } } diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 943cd0a..40adc97 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -10,7 +10,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Ping Castle")] [assembly: AssemblyProduct("Ping Castle")] -[assembly: AssemblyCopyright("Copyright © 2018 Ping Castle")] +[assembly: AssemblyCopyright("Copyright © 2019 Ping Castle")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -29,5 +29,5 @@ // Numéro de build // Révision // -[assembly: AssemblyVersion("2.5.2.0")] -[assembly: AssemblyFileVersion("2.5.2.0")] +[assembly: AssemblyVersion("2.6.0.0")] +[assembly: AssemblyFileVersion("2.6.0.0")] diff --git a/NullSession/lsa.cs b/RPC/lsa.cs similarity index 99% rename from NullSession/lsa.cs rename to RPC/lsa.cs index b728df3..07929bf 100644 --- a/NullSession/lsa.cs +++ b/RPC/lsa.cs @@ -12,7 +12,7 @@ using System.Security.Principal; using System.Text; -namespace PingCastle.NullSession +namespace PingCastle.RPC { [DebuggerDisplay("{DomainName}")] public class LSA_DOMAIN_INFORMATION @@ -229,6 +229,7 @@ public lsa() { InitializeStub(interfaceId, MIDL_ProcFormatStringx86, MIDL_TypeFormatStringx86, "\\pipe\\lsarpc", 0); } + UseNullSession = true; } [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)] diff --git a/NullSession/nativemethods.cs b/RPC/nativemethods.cs similarity index 86% rename from NullSession/nativemethods.cs rename to RPC/nativemethods.cs index e3661d3..e2ba534 100644 --- a/NullSession/nativemethods.cs +++ b/RPC/nativemethods.cs @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; using System.Text; -namespace PingCastle.NullSession +namespace PingCastle.RPC { internal class NativeMethods { @@ -24,11 +24,11 @@ internal class NativeMethods [DllImport("Rpcrt4.dll", EntryPoint = "NdrClientCall2", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode, SetLastError = false)] - internal static extern IntPtr NdrClientCall2x64(IntPtr pMIDL_STUB_DESC, IntPtr formatString, IntPtr intptrServer, int flag, ref PingCastle.NullSession.nrpc.NETLOGON_TRUSTED_DOMAIN_ARRAY output); + internal static extern IntPtr NdrClientCall2x64(IntPtr pMIDL_STUB_DESC, IntPtr formatString, IntPtr intptrServer, int flag, ref PingCastle.RPC.nrpc.NETLOGON_TRUSTED_DOMAIN_ARRAY output); [DllImport("Rpcrt4.dll", EntryPoint = "NdrClientCall2", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode, SetLastError = false)] - internal static extern IntPtr NdrClientCall2x64(IntPtr pMIDL_STUB_DESC, IntPtr formatString, IntPtr intptrSystemName, ref PingCastle.NullSession.lsa.LSAPR_OBJECT_ATTRIBUTES objectAttributes, UInt32 DesiredAccess, out IntPtr PolicyHandle); + internal static extern IntPtr NdrClientCall2x64(IntPtr pMIDL_STUB_DESC, IntPtr formatString, IntPtr intptrSystemName, ref PingCastle.RPC.lsa.LSAPR_OBJECT_ATTRIBUTES objectAttributes, UInt32 DesiredAccess, out IntPtr PolicyHandle); [DllImport("Rpcrt4.dll", EntryPoint = "NdrClientCall2", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode, SetLastError = false)] @@ -36,7 +36,7 @@ internal class NativeMethods [DllImport("Rpcrt4.dll", EntryPoint = "NdrClientCall2", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode, SetLastError = false)] - internal static extern IntPtr NdrClientCall2x64(IntPtr pMIDL_STUB_DESC, IntPtr formatString, IntPtr PolicyHandle, PingCastle.NullSession.lsa.LSAPR_SID_ENUM_BUFFER enumBuffer, out IntPtr IntPtrReferencedDomains, IntPtr IntPtrTranslatedNames, UInt32 LookupLevel, out UInt32 MappedCount); + internal static extern IntPtr NdrClientCall2x64(IntPtr pMIDL_STUB_DESC, IntPtr formatString, IntPtr PolicyHandle, PingCastle.RPC.lsa.LSAPR_SID_ENUM_BUFFER enumBuffer, out IntPtr IntPtrReferencedDomains, IntPtr IntPtrTranslatedNames, UInt32 LookupLevel, out UInt32 MappedCount); [DllImport("Rpcrt4.dll", EntryPoint = "NdrClientCall2", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode, SetLastError = false)] @@ -62,6 +62,14 @@ internal class NativeMethods CharSet = CharSet.Unicode, SetLastError = false)] internal static extern IntPtr NdrClientCall2x86(IntPtr pMIDL_STUB_DESC, IntPtr formatString, IntPtr args); + [DllImport("Rpcrt4.dll", EntryPoint = "NdrClientCall2", CallingConvention = CallingConvention.Cdecl, + CharSet = CharSet.Unicode, SetLastError = false)] + internal static extern IntPtr NdrClientCall2x64(IntPtr intPtr1, IntPtr intPtr2, string pPrinterName, out IntPtr pHandle, string pDatatype, ref rprn.DEVMODE_CONTAINER pDevModeContainer, int AccessRequired); + + [DllImport("Rpcrt4.dll", EntryPoint = "NdrClientCall2", CallingConvention = CallingConvention.Cdecl, + CharSet = CharSet.Unicode, SetLastError = false)] + internal static extern IntPtr NdrClientCall2x64(IntPtr intPtr1, IntPtr intPtr2, IntPtr hPrinter, uint fdwFlags, uint fdwOptions, string pszLocalMachine, uint dwPrinterLocal, IntPtr intPtr3); + [DllImport("Rpcrt4.dll", EntryPoint = "RpcBindingFree", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = false)] internal static extern Int32 RpcBindingFree(ref IntPtr lpString); @@ -122,5 +130,6 @@ internal static extern Int32 RpcBindingSetAuthInfo(IntPtr lpBinding, string Serv [DllImport("advapi32.dll", SetLastError = true)] internal static extern IntPtr GetSidSubAuthorityCount(IntPtr psid); - } + + } } diff --git a/NullSession/nrpc.cs b/RPC/nrpc.cs similarity index 98% rename from NullSession/nrpc.cs rename to RPC/nrpc.cs index 6ca1851..37350f3 100644 --- a/NullSession/nrpc.cs +++ b/RPC/nrpc.cs @@ -14,7 +14,7 @@ using System.Security.Principal; using System.Text; -namespace PingCastle.NullSession +namespace PingCastle.RPC { [DebuggerDisplay("{DnsDomainName} {NetbiosDomainName}")] @@ -93,7 +93,7 @@ private struct DS_DOMAIN_TRUSTSW } [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] - public nrpc() + public nrpc(bool WillUseNullSession = true) { Guid interfaceId = new Guid("12345678-1234-ABCD-EF00-01234567CFFB"); if (IntPtr.Size == 8) @@ -104,6 +104,7 @@ public nrpc() { InitializeStub(interfaceId, MIDL_ProcFormatStringx86, MIDL_TypeFormatStringx86, "\\pipe\\netlogon"); } + UseNullSession = WillUseNullSession; } [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)] diff --git a/NullSession/nullsession.cs b/RPC/nullsession.cs similarity index 99% rename from NullSession/nullsession.cs rename to RPC/nullsession.cs index 8c2f02a..8b88d76 100644 --- a/NullSession/nullsession.cs +++ b/RPC/nullsession.cs @@ -12,7 +12,7 @@ using System.Security.Principal; using System.Text; -namespace PingCastle.NullSession +namespace PingCastle.RPC { public enum TypeOfEnumeration { diff --git a/NullSession/rpcapi.cs b/RPC/rpcapi.cs similarity index 91% rename from NullSession/rpcapi.cs rename to RPC/rpcapi.cs index 68f459c..9fa934f 100644 --- a/NullSession/rpcapi.cs +++ b/RPC/rpcapi.cs @@ -12,7 +12,7 @@ using System.Security.Permissions; using System.Text; -namespace PingCastle.NullSession +namespace PingCastle.RPC { public abstract class rpcapi { @@ -33,7 +33,7 @@ public abstract class rpcapi allocmemory AllocateMemoryDelegate = AllocateMemory; freememory FreeMemoryDelegate = FreeMemory; - + public bool UseNullSession { get; set; } // 5 seconds public UInt32 RPCTimeOut = 5000; @@ -245,29 +245,32 @@ protected IntPtr Bind (IntPtr IntPtrserver) Trace.WriteLine("RpcBindingFromStringBinding failed with status 0x" + status.ToString("x")); return IntPtr.Zero; } - // note: windows xp doesn't support user or domain = "" => return 0xE - NativeMethods.SEC_WINNT_AUTH_IDENTITY identity = new NativeMethods.SEC_WINNT_AUTH_IDENTITY(); - identity.User = ""; - identity.UserLength = identity.User.Length * 2; - identity.Domain = ""; - identity.DomainLength = identity.Domain.Length * 2; - identity.Password = ""; - identity.Flags = 2; - - NativeMethods.RPC_SECURITY_QOS qos = new NativeMethods.RPC_SECURITY_QOS(); - qos.Version = 1; - qos.ImpersonationType = 3; - GCHandle qoshandle = GCHandle.Alloc(qos, GCHandleType.Pinned); - - // 9 = negotiate , 10 = ntlm ssp - status = NativeMethods.RpcBindingSetAuthInfoEx(binding, server, 0, 9, ref identity, 0, ref qos); - qoshandle.Free(); - if (status != 0) - { - Trace.WriteLine("RpcBindingSetAuthInfoEx failed with status 0x" + status.ToString("x")); - Unbind(IntPtrserver, binding); - return IntPtr.Zero; - } + if (UseNullSession) + { + // note: windows xp doesn't support user or domain = "" => return 0xE + NativeMethods.SEC_WINNT_AUTH_IDENTITY identity = new NativeMethods.SEC_WINNT_AUTH_IDENTITY(); + identity.User = ""; + identity.UserLength = identity.User.Length * 2; + identity.Domain = ""; + identity.DomainLength = identity.Domain.Length * 2; + identity.Password = ""; + identity.Flags = 2; + + NativeMethods.RPC_SECURITY_QOS qos = new NativeMethods.RPC_SECURITY_QOS(); + qos.Version = 1; + qos.ImpersonationType = 3; + GCHandle qoshandle = GCHandle.Alloc(qos, GCHandleType.Pinned); + + // 9 = negotiate , 10 = ntlm ssp + status = NativeMethods.RpcBindingSetAuthInfoEx(binding, server, 0, 9, ref identity, 0, ref qos); + qoshandle.Free(); + if (status != 0) + { + Trace.WriteLine("RpcBindingSetAuthInfoEx failed with status 0x" + status.ToString("x")); + Unbind(IntPtrserver, binding); + return IntPtr.Zero; + } + } status = NativeMethods.RpcBindingSetOption(binding, 12, RPCTimeOut); if (status != 0) diff --git a/NullSession/samr.cs b/RPC/samr.cs similarity index 99% rename from NullSession/samr.cs rename to RPC/samr.cs index f00be73..5a1d7d9 100644 --- a/NullSession/samr.cs +++ b/RPC/samr.cs @@ -13,7 +13,7 @@ using System.Security.Principal; using System.Text; -namespace PingCastle.NullSession +namespace PingCastle.RPC { [DebuggerDisplay("{Name}")] @@ -123,6 +123,7 @@ public samr() { InitializeStub(interfaceId, MIDL_ProcFormatStringx86, MIDL_TypeFormatStringx86, "\\pipe\\samr"); } + UseNullSession = true; } [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)] diff --git a/RPC/spool.cs b/RPC/spool.cs new file mode 100644 index 0000000..601a166 --- /dev/null +++ b/RPC/spool.cs @@ -0,0 +1,372 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Security.Permissions; +using System.Text; + +namespace PingCastle.RPC +{ + public class rprn : rpcapi + { + private static byte[] MIDL_ProcFormatStringx86 = new byte[] { + 0x00,0x48,0x00,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70, + 0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x01,0x00,0x18,0x00,0x31,0x04,0x00,0x00,0x00,0x5c,0x08,0x00,0x40,0x00,0x46,0x06,0x08,0x05, + 0x00,0x00,0x01,0x00,0x00,0x00,0x0b,0x00,0x00,0x00,0x02,0x00,0x10,0x01,0x04,0x00,0x0a,0x00,0x0b,0x00,0x08,0x00,0x02,0x00,0x0b,0x01,0x0c,0x00,0x1e, + 0x00,0x48,0x00,0x10,0x00,0x08,0x00,0x70,0x00,0x14,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x02,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00, + 0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x03,0x00,0x08,0x00,0x32, + 0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00, + 0x04,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00, + 0x48,0x00,0x00,0x00,0x00,0x05,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00, + 0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x06,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00, + 0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x07,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01, + 0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x08,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00, + 0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x09,0x00,0x08,0x00, + 0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00, + 0x00,0x0a,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00, + 0x00,0x48,0x00,0x00,0x00,0x00,0x0b,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70, + 0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x0c,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00, + 0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x0d,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44, + 0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x0e,0x00,0x08,0x00,0x32,0x00,0x00,0x00, + 0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x0f,0x00,0x08, + 0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00, + 0x00,0x00,0x10,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08, + 0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x11,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00, + 0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x12,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00, + 0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x13,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00, + 0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x14,0x00,0x08,0x00,0x32,0x00,0x00, + 0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x15,0x00, + 0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00, + 0x00,0x00,0x00,0x16,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00, + 0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x17,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00, + 0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x18,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01, + 0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x19,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08, + 0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x1a,0x00,0x08,0x00,0x32,0x00, + 0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x1b, + 0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48, + 0x00,0x00,0x00,0x00,0x1c,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04, + 0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x1d,0x00,0x08,0x00,0x30,0xe0,0x00,0x00,0x00,0x00,0x38,0x00,0x40,0x00,0x44,0x02,0x08,0x01,0x00,0x00, + 0x00,0x00,0x00,0x00,0x18,0x01,0x00,0x00,0x36,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x1e,0x00,0x08,0x00,0x32,0x00,0x00, + 0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x1f,0x00, + 0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00, + 0x00,0x00,0x00,0x20,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00, + 0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x21,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00, + 0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x22,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01, + 0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x23,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08, + 0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x24,0x00,0x08,0x00,0x32,0x00, + 0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x25, + 0x00,0x04,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x26,0x00, + 0x04,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x27,0x00,0x08, + 0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00, + 0x00,0x00,0x28,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08, + 0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x29,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00, + 0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x2a,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00, + 0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x2b,0x00,0x04,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x40,0x00,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x2c,0x00,0x04,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40, + 0x00,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x2d,0x00,0x04,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00, + 0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x2e,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08, + 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x2f,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00, + 0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x30,0x00,0x08,0x00,0x32, + 0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00, + 0x31,0x00,0x04,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x32, + 0x00,0x04,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x33,0x00, + 0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00, + 0x00,0x00,0x00,0x34,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00, + 0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x35,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00, + 0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x36,0x00,0x04,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x08,0x01, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x37,0x00,0x04,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x08,0x01,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x38,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00, + 0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x39,0x00,0x04,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40, + 0x00,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x3a,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01, + 0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x3b,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00, + 0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x3c,0x00,0x08,0x00, + 0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00, + 0x00,0x3d,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x04,0x00,0x08,0x00, + 0x00,0x48,0x00,0x00,0x00,0x00,0x3e,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x08,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x70, + 0x00,0x04,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x3f,0x00,0x04,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x08,0x01,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x40,0x00,0x04,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x08,0x01,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x41,0x00,0x1c,0x00,0x30,0x40,0x00,0x00,0x00,0x00,0x3c,0x00,0x08,0x00,0x46,0x07,0x08,0x05,0x00,0x00, + 0x01,0x00,0x00,0x00,0x08,0x00,0x00,0x00,0x3a,0x00,0x48,0x00,0x04,0x00,0x08,0x00,0x48,0x00,0x08,0x00,0x08,0x00,0x0b,0x00,0x0c,0x00,0x02,0x00,0x48, + 0x00,0x10,0x00,0x08,0x00,0x0b,0x00,0x14,0x00,0x3e,0x00,0x70,0x00,0x18,0x00,0x08,0x00,0x00 + }; + + private static byte[] MIDL_ProcFormatStringx64 = new byte[] { + 0x00,0x48,0x00,0x00,0x00,0x00,0x00,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x01,0x00,0x30,0x00,0x31,0x08,0x00,0x00,0x00,0x5c,0x08,0x00,0x40,0x00,0x46,0x06, + 0x0a,0x05,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x0b,0x00,0x00,0x00,0x02,0x00,0x10,0x01,0x08,0x00,0x0a,0x00,0x0b,0x00,0x10,0x00,0x02,0x00,0x0b, + 0x01,0x18,0x00,0x1e,0x00,0x48,0x00,0x20,0x00,0x08,0x00,0x70,0x00,0x28,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x02,0x00,0x10,0x00,0x32,0x00, + 0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00, + 0x00,0x03,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00, + 0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x04,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x05,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01, + 0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x06,0x00,0x10,0x00,0x32,0x00,0x00, + 0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00, + 0x07,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08, + 0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x08,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x09,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a, + 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x0a,0x00,0x10,0x00,0x32,0x00,0x00,0x00, + 0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x0b, + 0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00, + 0x00,0x48,0x00,0x00,0x00,0x00,0x0c,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x0d,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x0e,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00, + 0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x0f,0x00, + 0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00, + 0x48,0x00,0x00,0x00,0x00,0x10,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x11,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x12,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00, + 0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x13,0x00,0x10, + 0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48, + 0x00,0x00,0x00,0x00,0x14,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70, + 0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x15,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x16,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08, + 0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x17,0x00,0x10,0x00, + 0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00, + 0x00,0x00,0x00,0x18,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00, + 0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x19,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x1a,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00, + 0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x1b,0x00,0x10,0x00,0x32, + 0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00, + 0x00,0x00,0x1c,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08, + 0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x1d,0x00,0x10,0x00,0x30,0xe0,0x00,0x00,0x00,0x00,0x38,0x00,0x40,0x00,0x44,0x02,0x0a,0x01,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x01,0x00,0x00,0x32,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x1e,0x00,0x10,0x00,0x32, + 0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00, + 0x00,0x00,0x1f,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08, + 0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x20,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x21,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44, + 0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x22,0x00,0x10,0x00,0x32,0x00, + 0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00, + 0x00,0x23,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00, + 0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x24,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x25,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00, + 0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x26,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40, + 0x00,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x27,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00, + 0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x28,0x00,0x10,0x00,0x32, + 0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00, + 0x00,0x00,0x29,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08, + 0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x2a,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x2b,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40, + 0x00,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x2c,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x40,0x00,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x2d,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x40,0x00,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x2e,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00, + 0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x2f,0x00,0x10, + 0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48, + 0x00,0x00,0x00,0x00,0x30,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70, + 0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x31,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x0a,0x01,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x32,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x0a,0x01,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x33,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x34,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00, + 0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x35,0x00, + 0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00, + 0x48,0x00,0x00,0x00,0x00,0x36,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x48,0x00,0x00,0x00,0x00,0x37,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x38,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x39,0x00,0x08,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x0a, + 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x3a,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01, + 0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x3b,0x00,0x10,0x00,0x32,0x00,0x00, + 0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00, + 0x3c,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08, + 0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x3d,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x3e,0x00,0x10,0x00,0x32,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x44,0x01,0x0a, + 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x70,0x00,0x08,0x00,0x08,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x3f,0x00,0x08,0x00,0x32,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x40,0x00,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x40,0x00,0x08,0x00,0x32,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x0a,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x41,0x00,0x38,0x00,0x30,0x40, + 0x00,0x00,0x00,0x00,0x3c,0x00,0x08,0x00,0x46,0x07,0x0a,0x05,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x08,0x00,0x00,0x00,0x36,0x00,0x48,0x00,0x08, + 0x00,0x08,0x00,0x48,0x00,0x10,0x00,0x08,0x00,0x0b,0x00,0x18,0x00,0x02,0x00,0x48,0x00,0x20,0x00,0x08,0x00,0x0b,0x00,0x28,0x00,0x3a,0x00,0x70,0x00, + 0x30,0x00,0x08,0x00,0x00 + + }; + + private static byte[] MIDL_TypeFormatStringx86 = new byte[] { + 0x00,0x00,0x12,0x08,0x25,0x5c,0x11,0x04,0x02,0x00,0x30,0xa0,0x00,0x00,0x11,0x00,0x0e,0x00,0x1b,0x00,0x01,0x00,0x19,0x00,0x00,0x00,0x01,0x00,0x01, + 0x5b,0x16,0x03,0x08,0x00,0x4b,0x5c,0x46,0x5c,0x04,0x00,0x04,0x00,0x12,0x00,0xe6,0xff,0x5b,0x08,0x08,0x5b,0x11,0x04,0x02,0x00,0x30,0xe1,0x00,0x00, + 0x30,0x41,0x00,0x00,0x12,0x00,0x48,0x00,0x1b,0x01,0x02,0x00,0x19,0x00,0x0c,0x00,0x01,0x00,0x06,0x5b,0x16,0x03,0x14,0x00,0x4b,0x5c,0x46,0x5c,0x10, + 0x00,0x10,0x00,0x12,0x00,0xe6,0xff,0x5b,0x06,0x06,0x08,0x08,0x08,0x08,0x5b,0x1b,0x03,0x14,0x00,0x19,0x00,0x08,0x00,0x01,0x00,0x4b,0x5c,0x48,0x49, + 0x14,0x00,0x00,0x00,0x01,0x00,0x10,0x00,0x10,0x00,0x12,0x00,0xc2,0xff,0x5b,0x4c,0x00,0xc9,0xff,0x5b,0x16,0x03,0x10,0x00,0x4b,0x5c,0x46,0x5c,0x0c, + 0x00,0x0c,0x00,0x12,0x00,0xd0,0xff,0x5b,0x08,0x08,0x08,0x08,0x5b,0x00 + }; + + private static byte[] MIDL_TypeFormatStringx64 = new byte[] { + 0x00,0x00,0x12,0x08,0x25,0x5c,0x11,0x04,0x02,0x00,0x30,0xa0,0x00,0x00,0x11,0x00,0x0e,0x00,0x1b,0x00,0x01,0x00,0x19,0x00,0x00,0x00,0x01,0x00,0x01, + 0x5b,0x1a,0x03,0x10,0x00,0x00,0x00,0x06,0x00,0x08,0x40,0x36,0x5b,0x12,0x00,0xe6,0xff,0x11,0x04,0x02,0x00,0x30,0xe1,0x00,0x00,0x30,0x41,0x00,0x00, + 0x12,0x00,0x38,0x00,0x1b,0x01,0x02,0x00,0x19,0x00,0x0c,0x00,0x01,0x00,0x06,0x5b,0x1a,0x03,0x18,0x00,0x00,0x00,0x0a,0x00,0x06,0x06,0x08,0x08,0x08, + 0x36,0x5c,0x5b,0x12,0x00,0xe2,0xff,0x21,0x03,0x00,0x00,0x19,0x00,0x08,0x00,0x01,0x00,0xff,0xff,0xff,0xff,0x00,0x00,0x4c,0x00,0xda,0xff,0x5c,0x5b, + 0x1a,0x03,0x18,0x00,0x00,0x00,0x08,0x00,0x08,0x08,0x08,0x40,0x36,0x5b,0x12,0x00,0xda,0xff,0x00 + }; + + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + public rprn() + { + Guid interfaceId = new Guid("12345678-1234-ABCD-EF00-0123456789AB"); + if (IntPtr.Size == 8) + { + InitializeStub(interfaceId, MIDL_ProcFormatStringx64, MIDL_TypeFormatStringx64, "\\pipe\\spoolss"); + } + else + { + InitializeStub(interfaceId, MIDL_ProcFormatStringx86, MIDL_TypeFormatStringx86, "\\pipe\\spoolss"); + } + } + + [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)] + ~rprn() + { + freeStub(); + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct DEVMODE_CONTAINER + { + Int32 cbBuf; + IntPtr pDevMode; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct RPC_V2_NOTIFY_OPTIONS_TYPE + { + UInt16 Type; + UInt16 Reserved0; + UInt32 Reserved1; + UInt32 Reserved2; + UInt32 Count; + IntPtr pFields; + }; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct RPC_V2_NOTIFY_OPTIONS + { + UInt32 Version; + UInt32 Reserved; + UInt32 Count; + /* [unique][size_is] */ + RPC_V2_NOTIFY_OPTIONS_TYPE pTypes; + }; + + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + public Int32 RpcOpenPrinter(string pPrinterName, out IntPtr pHandle, string pDatatype, ref DEVMODE_CONTAINER pDevModeContainer, Int32 AccessRequired) + { + IntPtr result = IntPtr.Zero; + IntPtr intptrPrinterName = Marshal.StringToHGlobalUni(pPrinterName); + IntPtr intptrDatatype = Marshal.StringToHGlobalUni(pDatatype); + pHandle = IntPtr.Zero; + try + { + if (IntPtr.Size == 8) + { + result = NativeMethods.NdrClientCall2x64(GetStubHandle(), GetProcStringHandle(36), pPrinterName, out pHandle, pDatatype, ref pDevModeContainer, AccessRequired); + } + else + { + IntPtr tempValue = IntPtr.Zero; + GCHandle handle = GCHandle.Alloc(tempValue, GCHandleType.Pinned); + IntPtr tempValuePointer = handle.AddrOfPinnedObject(); + GCHandle handleDevModeContainer = GCHandle.Alloc(pDevModeContainer, GCHandleType.Pinned); + IntPtr tempValueDevModeContainer = handleDevModeContainer.AddrOfPinnedObject(); + try + { + result = CallNdrClientCall2x86(34, intptrPrinterName, tempValuePointer, intptrDatatype, tempValueDevModeContainer, new IntPtr(AccessRequired)); + // each pinvoke work on a copy of the arguments (without an out specifier) + // get back the data + pHandle = Marshal.ReadIntPtr(tempValuePointer); + } + finally + { + handle.Free(); + handleDevModeContainer.Free(); + } + } + } + catch (SEHException) + { + Trace.WriteLine("RpcOpenPrinter failed 0x" + Marshal.GetExceptionCode().ToString("x")); + return Marshal.GetExceptionCode(); + } + finally + { + if (intptrPrinterName != IntPtr.Zero) + Marshal.FreeHGlobal(intptrPrinterName); + if (intptrDatatype != IntPtr.Zero) + Marshal.FreeHGlobal(intptrDatatype); + } + return (int)result.ToInt64(); + } + + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + public Int32 RpcClosePrinter(ref IntPtr ServerHandle) + { + IntPtr result = IntPtr.Zero; + try + { + if (IntPtr.Size == 8) + { + result = NativeMethods.NdrClientCall2x64(GetStubHandle(), GetProcStringHandle(1076), ref ServerHandle); + } + else + { + IntPtr tempValue = ServerHandle; + GCHandle handle = GCHandle.Alloc(tempValue, GCHandleType.Pinned); + IntPtr tempValuePointer = handle.AddrOfPinnedObject(); + try + { + result = CallNdrClientCall2x86(1018, tempValuePointer); + // each pinvoke work on a copy of the arguments (without an out specifier) + // get back the data + ServerHandle = Marshal.ReadIntPtr(tempValuePointer); + } + finally + { + handle.Free(); + } + } + } + catch (SEHException) + { + Trace.WriteLine("RpcClosePrinter failed 0x" + Marshal.GetExceptionCode().ToString("x")); + return Marshal.GetExceptionCode(); + } + return (int)result.ToInt64(); + } + + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + public Int32 RpcRemoteFindFirstPrinterChangeNotificationEx( + /* [in] */ IntPtr hPrinter, + /* [in] */ UInt32 fdwFlags, + /* [in] */ UInt32 fdwOptions, + /* [unique][string][in] */ string pszLocalMachine, + /* [in] */ UInt32 dwPrinterLocal) + { + IntPtr result = IntPtr.Zero; + IntPtr intptrLocalMachine = Marshal.StringToHGlobalUni(pszLocalMachine); + try + { + if (IntPtr.Size == 8) + { + result = NativeMethods.NdrClientCall2x64(GetStubHandle(), GetProcStringHandle(2308), hPrinter, fdwFlags, fdwOptions, pszLocalMachine, dwPrinterLocal, IntPtr.Zero); + } + else + { + try + { + result = CallNdrClientCall2x86(2178, hPrinter, new IntPtr(fdwFlags), new IntPtr(fdwOptions), intptrLocalMachine, new IntPtr(dwPrinterLocal), IntPtr.Zero); + // each pinvoke work on a copy of the arguments (without an out specifier) + // get back the data + } + finally + { + } + } + } + catch (SEHException) + { + Trace.WriteLine("RpcRemoteFindFirstPrinterChangeNotificationEx failed 0x" + Marshal.GetExceptionCode().ToString("x")); + return Marshal.GetExceptionCode(); + } + finally + { + if (intptrLocalMachine != IntPtr.Zero) + Marshal.FreeHGlobal(intptrLocalMachine); + } + return (int)result.ToInt64(); + } + } +} diff --git a/Report/HealthCheckReportBase.cs b/Report/HealthCheckReportBase.cs deleted file mode 100644 index 53b232e..0000000 --- a/Report/HealthCheckReportBase.cs +++ /dev/null @@ -1,552 +0,0 @@ -// -// Copyright (c) Ping Castle. All rights reserved. -// https://www.pingcastle.com -// -// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. -// -using PingCastle.template; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Globalization; -using System.IO; -using System.Reflection; -using System.Text; - -namespace PingCastle.Healthcheck -{ - public abstract class HealthCheckReportBase - { - - protected StringBuilder sb = new StringBuilder(); - public delegate string GetUrlDelegate(DomainKey domainKey, string displayName); - public delegate bool HasDomainAmbigousNameDelegate(DomainKey domainKey); - - public GetUrlDelegate GetUrlCallback { get; set; } - public HasDomainAmbigousNameDelegate HasDomainAmbigousName { get; set; } - - public string GenerateReportFile(string filename) - { - string html = TemplateManager.LoadResponsiveTemplate(); - - html = html.Replace("<%=Title%>", GenerateTitleInformation()); - html = html.Replace("<%=Header%>", GenerateHeaderInformation()); - - html = html.Replace("<%=Body%>", GenerateBodyInformation()); - html = html.Replace("<%=Footer%>", GenerateFooterInformation()); - Hook(ref html); - File.WriteAllText(filename, html); - return html; - } - - protected virtual void Hook(ref string html) - { - - } - - protected void Add(int value) - { - sb.Append(value); - } - - protected void Add(bool value) - { - sb.Append(value); - } - - protected void Add(string text) - { - sb.Append(text); - } - - protected void Add(DateTime date) - { - sb.Append(date.ToString("u")); - } - - public override string ToString() - { - return sb.ToString(); - } - - protected delegate void GenerateContentDelegate(); - - protected abstract string GenerateFooterInformation(); - - protected abstract string GenerateTitleInformation(); - - protected abstract string GenerateHeaderInformation(); - - protected abstract string GenerateBodyInformation(); - - public static bool NoContainer = false; - - public static string GetStyleSheetTheme() - { - return @" - -"; - } - - protected void GenerateSection(string title, GenerateContentDelegate generateContent) - { - string id = "section" + title.Replace(" ", ""); - Add(@" - -
-
-
-
- -
-"); - generateContent(); - Add(@" -
-
-
-
-
- -"); - } - - protected void GenerateSubSection(string title) - { - Add(@" - -
-

" + title + @"

-
- "); - } - - protected void GenerateAccordion(string accordion, GenerateContentDelegate content) - { - Add(@" - -
-
-
-"); - content(); - Add(@" -
-
-
- -"); - } - - - // see https://msdn.microsoft.com/en-us/library/cc223741.aspx - // 6.1.4.2 msDS-Behavior-Version: DC Functional Level - public static string DecodeDomainFunctionalLevel(int DomainFunctionalLevel) - { - switch (DomainFunctionalLevel) - { - case 0: - return "Windows 2000"; - case 1: - return "Windows Server 2003 interim"; - case 2: - return "Windows Server 2003"; - case 3: - return "Windows Server 2008"; - case 4: - return "Windows Server 2008 R2"; - case 5: - return "Windows Server 2012"; - case 6: - return "Windows Server 2012 R2"; - case 7: - return "Windows Server 2016"; - default: - return "Unknown: " + DomainFunctionalLevel; - } - } - - // see https://msdn.microsoft.com/en-us/library/cc223743.aspx - // 6.1.4.4 msDS-Behavior-Version: Forest Functional Level - public static string DecodeForestFunctionalLevel(int ForestFunctionalLevel) - { - switch (ForestFunctionalLevel) - { - case 0: - return "Windows 2000"; - case 1: - return "Windows Server 2003 mixed"; - case 2: - return "Windows Server 2003"; - case 3: - return "Windows Server 2008"; - case 4: - return "Windows Server 2008 R2"; - case 5: - return "Windows Server 2012"; - case 6: - return "Windows Server 2012 R2"; - case 7: - return "Windows Server 2016"; - default: - return "Unknown: " + ForestFunctionalLevel; - } - } - - protected int OrderOS(string os1, string os2) - { - int ios1 = OSToInt(os1); - int ios2 = OSToInt(os2); - if (ios1 > 0 && ios2 > 0) - { - if (ios1 > ios2) - return 1; - else if (ios1 < ios2) - return -1; - return 0; - } - else if (ios1 > 0) - return -1; - else if (ios2 > 0) - return 1; - return String.Compare(os1, os2); - } - - protected int OSToInt(string os) - { - switch (os) - { - case "Windows XP": - return 1; - case "Windows Vista": - return 2; - case "Windows 7": - return 3; - case "Windows 8": - return 4; - case "Windows 10": - return 5; - case "Windows NT": - return 6; - case "Windows 2000": - return 7; - case "Windows 2003": - return 8; - case "Windows 2008": - return 9; - case "Windows 2012": - return 10; - case "Windows 2016": - return 11; - case "Windows Embedded": - return 12; - case "OperatingSystem not set": - return 13; - } - return 0; - } - - protected string GetPSOStringValue(GPPSecurityPolicy policy, string propertyName) - { - foreach (var property in policy.Properties) - { - if (property.Property == propertyName) - { - if (property.Value == 0) - { - if (propertyName == "PasswordComplexity") - { - return "False"; - } - if (propertyName == "ClearTextPassword" - || propertyName == "ScreenSaveActive" || propertyName == "ScreenSaverIsSecure") - return "False"; - - } - if (property.Value == -1 && propertyName == "MaximumPasswordAge") - { - return "Never expires"; - } - if (property.Value == 1) - { - if (propertyName == "ClearTextPassword") - return "True"; - if (propertyName == "PasswordComplexity" - || propertyName == "ScreenSaveActive" || propertyName == "ScreenSaverIsSecure") - return "True"; - } - if (propertyName == "MinimumPasswordLength") - { - if (property.Value < 8) - { - return "" + property.Value.ToString() + ""; - } - } - if (propertyName == "MinimumPasswordAge") - { - if (property.Value == 0) - { - return "0 day"; - } - return property.Value.ToString() + " day(s)"; - } - if (propertyName == "MaximumPasswordAge") - { - return property.Value.ToString() + " day(s)"; - } - if (propertyName == "ResetLockoutCount" || propertyName == "LockoutDuration") - { - if (property.Value <= 0) - return "Infinite"; - return property.Value.ToString() + " minute(s)"; - } - return property.Value.ToString(); - } - } - return "Not Set"; - } - - protected string GetLinkForLsaSetting(string property) - { - switch (property.ToLowerInvariant()) - { - case "enableguestaccount": - return @"EnableGuestAccount"; - case "lsaanonymousnamelookup": - return @"LSAAnonymousNameLookup"; - case "everyoneincludesanonymous": - return @"EveryoneIncludesAnonymous"; - case "limitblankpassworduse": - return @"LimitBlankPasswordUse"; - case "forceguest": - return @"ForceGuest"; - case "lmcompatibilitylevel": - return @"LmCompatibilityLevel"; - case "NoLMHash": - return @"NoLMHash"; - case "restrictanonymous": - return @"RestrictAnonymous"; - case "restrictanonymoussam": - return @"RestrictAnonymousSam"; - - } - return property; - } - - protected string GetEnumDescription(HealthcheckRiskModelCategory value) - { - // Get the Description attribute value for the enum value - FieldInfo fi = value.GetType().GetField(value.ToString()); - DescriptionAttribute[] attributes = - (DescriptionAttribute[])fi.GetCustomAttributes( - typeof(DescriptionAttribute), false); - - if (attributes.Length > 0) - { - return attributes[0].Description; - } - else - { - return value.ToString(); - } - } - - protected void GenerateGauge(int percentage) - { - Add(@"050100"); - } - - protected void GenerateChartNeedle(int percentage) - { - double leftX, leftY, rightX, rightY, thetaRad, topX, topY; - NumberFormatInfo nfi = new NumberFormatInfo(); - nfi.NumberDecimalSeparator = "."; - thetaRad = percentage * Math.PI / 100; - topX = -144 * Math.Cos(thetaRad); - topY = -144 * Math.Sin(thetaRad); - leftX = -18 * Math.Cos(thetaRad - Math.PI / 2); - leftY = -18 * Math.Sin(thetaRad - Math.PI / 2); - rightX = -18 * Math.Cos(thetaRad + Math.PI / 2); - rightY = -18 * Math.Sin(thetaRad + Math.PI / 2); - Add("M "); - Add(leftX.ToString(nfi)); - Add(" "); - Add(leftY.ToString(nfi)); - Add(" L "); - Add(topX.ToString(nfi)); - Add(" "); - Add(topY.ToString(nfi)); - Add(" L "); - Add(rightX.ToString(nfi)); - Add(" "); - Add(rightY.ToString(nfi)); - } - - protected string Encode(string stringToencode) - { - // could have use HttpUtility.HtmlEncode but not dotnet core compliant - if (string.IsNullOrEmpty(stringToencode)) return stringToencode; - - string returnString = stringToencode; - - returnString = returnString.Replace("&", "&"); - returnString = returnString.Replace("'", "'"); - returnString = returnString.Replace("\"", """); - returnString = returnString.Replace(">", ">"); - returnString = returnString.Replace("<", "<"); - - return returnString; - } - - protected string PrintDomain(DomainKey key) - { - string label = key.DomainName; - if (GetUrlCallback == null) - return label; - string htmlData = GetUrlCallback(key, label); - if (String.IsNullOrEmpty(htmlData)) - return label; - return htmlData; - } - } -} diff --git a/Report/HealthCheckReportCompromiseGraph.cs b/Report/HealthCheckReportCompromiseGraph.cs deleted file mode 100644 index 93daac0..0000000 --- a/Report/HealthCheckReportCompromiseGraph.cs +++ /dev/null @@ -1,569 +0,0 @@ -// -// Copyright (c) Ping Castle. All rights reserved. -// https://www.pingcastle.com -// -// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. -// -using PingCastle.Data; -using PingCastle.template; -using System; -using System.Collections.Generic; -using System.Text; - -namespace PingCastle.Healthcheck -{ - public class HealthCheckReportCompromiseGraph : HealthCheckReportBase - { - private CompromiseGraphData CompromiseGraphData; - private readonly ADHealthCheckingLicense _license; - - public HealthCheckReportCompromiseGraph(CompromiseGraphData compromiseGraphData, ADHealthCheckingLicense license) - { - this.CompromiseGraphData = compromiseGraphData; - _license = license; - CompromiseGraphData.Data.Sort( - (SingleCompromiseGraphData a, SingleCompromiseGraphData b) - => - { - return String.Compare(a.Description, b.Description); - }); - } - - protected override void Hook(ref string html) - { - // full screen graphs - html = html.Replace("", ""); - html = html.Replace("", ""); - } - - protected override string GenerateTitleInformation() - { - return "PingCastle Compromission Graphs - " + DateTime.Now.ToString("yyyy-MM-dd"); - } - - protected override string GenerateHeaderInformation() - { - StringBuilder sb = new StringBuilder(); - sb.Append(@""); - sb.Append(HealthCheckReportBase.GetStyleSheetTheme()); - sb.Append(@""); - return sb.ToString(); - } - - protected override string GenerateBodyInformation() - { - StringBuilder sb = new StringBuilder(); - sb.Append(@" - - -
-
- -
-
-

About

-
-
-
-
-

This page has been inspired from the tools Active Directory Control Paths, OVALI and BloodHound.

-

Generated by Ping Castle all rights reserved

-

Open source components:

- -
-
-
-
- -
-
- -
-
- -
-
- -
-
-

Loading ...

-
-
-
-
- 0% -
-
-
-
- -
-
- -
-
-
-
- -

" + CompromiseGraphData.DomainFQDN + @" - Active Directory Compromission Graph

-

Date: " + CompromiseGraphData.GenerationDate.ToString("yyyy-MM-dd") + @" - Engine version: " + CompromiseGraphData.EngineVersion + @"

-
-
-
-
-This report has been generated with the "); - sb.Append(String.IsNullOrEmpty(_license.Edition) ? "Basic" : _license.Edition); - sb.Append(@" Edition of PingCastle."); - if (String.IsNullOrEmpty(_license.Edition)) - { - sb.Append(@" -
Being part of a commercial package is forbidden (selling the information contained in the report).
-If you are an auditor, you MUST purchase an Auditor license to share the development effort. -"); - } - sb.Append(@"
-

The goal is to understand if, by doing some actions, a user account can gain more privileges than expected. For example, if a helpdesk user can reset a password account which is the owner of the login script of a domain admin, this user can become domain administrator.

-

Users, groups and other objects are connected through arrows which explain these links. The more objects there are, the more care should be used to check the highlighted path.

-

The paths made by PingCastle have known limitations compared to other tools to produce its quick analysis:

-
    -
  • PingCastle does not check for local server ACL like bloodhound does (file server, etc)
  • -
  • PingCastle does only perform its analysis on a single path direction. The report to understand what a simple user can do is not present.
  • -
-

This is a compromise between speed and accuracy.

-

Select a graph to show its data then select Active Directory Compromission Graph to get back to this page again.

-
-
-
- - - - -"); - for (int i = 0; i < CompromiseGraphData.Data.Count; i++) - { - var data = CompromiseGraphData.Data[i]; - sb.Append(@""); - } - sb.Append(@" -
Group or user account ?On demand analysis ?Number of Account ?Presence of unusual group ?
"); - sb.Append(data.Description); - sb.Append(@""); - if (data.OnDemandAnalysis) - { - sb.Append(@"✓"); - } - sb.Append(@""); - int count = 0; - foreach (var node in data.Nodes) - { - if (node.Distance == 0) - continue; - if (String.Equals(node.Type, "user", StringComparison.OrdinalIgnoreCase)) - count++; - } - sb.Append(count); - sb.Append(@""); - if (data.UnusualGroup) - sb.Append(@"YES"); - else - sb.Append(@"NO"); - sb.Append(@"
-
-
-
-
-"); - for (int i = 0; i < CompromiseGraphData.Data.Count; i++) - { - - sb.Append(@" - -
-
- -
- Legend:
- u user
- w external user or group
- m computer
- g group
- o OU
- x GPO
- ? Other -
-
-"); - } - sb.Append(@" -
-
"); - return sb.ToString(); - } - - protected override string GenerateFooterInformation() - { - StringBuilder sb = new StringBuilder(); - sb.Append(@" - -"); - return sb.ToString(); - } - - - string BuildJasonFromSingleCompromiseGraph(SingleCompromiseGraphData data) - { - StringBuilder output = new StringBuilder(); - Dictionary idconversiontable = new Dictionary(); - output.Append("{"); - // START OF NODES - - output.Append(" \"nodes\": ["); - // it is important to put the root node as the first node for correct display - for (int i = 0; i < data.Nodes.Count; i++) - { - var node = data.Nodes[i]; - if (i != 0) - output.Append(" },"); - output.Append(" {"); - output.Append(" \"id\": " + node.Id + ","); - output.Append(" \"name\": \"" + HealthCheckReportMapBuilder.EscapeJsonString(node.Name) + "\","); - output.Append(" \"type\": \"" + node.Type + "\","); - output.Append(" \"shortName\": \"" + HealthCheckReportMapBuilder.EscapeJsonString(node.ShortName) + "\","); - if (node.Distance == 0) - output.Append(" \"dist\": null"); - else - output.Append(" \"dist\": \"" + node.Distance + "\""); - } - output.Append(" }"); - output.Append(" ],"); - // END OF NODES - - // START LINKS - output.Append(" \"links\": ["); - // avoid a final "," - for (int i = 0; i < data.Links.Count; i++) - { - var relation = data.Links[i]; - if (i != 0) - output.Append(" },"); - - output.Append(" {"); - output.Append(" \"source\": " + relation.Source + ","); - output.Append(" \"target\": " + relation.Target + ","); - output.Append(" \"rels\": ["); - for (int j = 0; j < relation.Hints.Count; j++) - { - output.Append(" \"" + data.Links[i].Hints[j] + "\"" + (j == relation.Hints.Count - 1 ? String.Empty : ",")); - } - - output.Append(" ]"); - } - if (data.Links.Count > 0) - { - output.Append(" }"); - } - output.Append(" ]"); - // END OF LINKS - output.Append("}"); - return output.ToString(); - } - - } -} diff --git a/Report/HealthCheckReportConsolidation.cs b/Report/HealthCheckReportConsolidation.cs deleted file mode 100644 index c36ddfa..0000000 --- a/Report/HealthCheckReportConsolidation.cs +++ /dev/null @@ -1,1396 +0,0 @@ -// -// Copyright (c) Ping Castle. All rights reserved. -// https://www.pingcastle.com -// -// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. -// -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Text; - -namespace PingCastle.Healthcheck -{ - public class HealthCheckReportConsolidation : HealthCheckReportBase - { - private HealthcheckDataCollection consolidation; - - - public HealthCheckReportConsolidation(HealthcheckDataCollection consolidation) - { - this.consolidation = consolidation; - } - - protected override string GenerateFooterInformation() - { - return null; - } - - protected override string GenerateTitleInformation() - { - return "PingCastle Consolidation report - " + DateTime.Now.ToString("yyyy-MM-dd"); - } - - protected override string GenerateHeaderInformation() - { - return GetStyleSheetTheme() + GetStyleSheet(); - } - - public static string GetStyleSheet() - { - return @" - - - -"; - } - - protected override void Hook(ref string html) - { - html = html.Replace("", @""); - } - - protected override string GenerateBodyInformation() - { - Version version = Assembly.GetExecutingAssembly().GetName().Version; - string versionString = version.ToString(4); -#if DEBUG - versionString += " Beta"; -#endif - - string output = @" - -
- -

Consolidation

-

Date: " + DateTime.Now.ToString("yyyy-MM-dd") + @" - Engine version: " + versionString + @"

-
-" + GenerateContent() + @" -
-"; - return output; - } - - private static string GenerateId(string title) - { - return "section" + title.Replace(" ", ""); - } - - protected void GenerateSectionFluid(string title, GenerateContentDelegate generateContent, string selectedTab, bool defaultIfTabEmpty = false) - { - string id = GenerateId(title); - bool isActive = (String.IsNullOrEmpty(selectedTab) ? defaultIfTabEmpty : selectedTab == id); - Add(@" - -
-
-
-

"); - Add(title); - Add(@"

-
-"); - generateContent(); - Add(@" -
-
- -"); - } - - public string GenerateContent(string selectedTab = null) - { - Add(@" -
-
-
    "); - GenerateTabHeader("Active Directory Indicators", selectedTab, true); - GenerateTabHeader("Rules Matched", selectedTab); - GenerateTabHeader("Domain Information", selectedTab); - GenerateTabHeader("User Information", selectedTab); - GenerateTabHeader("Computer Information", selectedTab); - GenerateTabHeader("Admin Groups", selectedTab); - GenerateTabHeader("Trusts", selectedTab); - GenerateTabHeader("Anomalies", selectedTab); - GenerateTabHeader("Password Policies", selectedTab); - GenerateTabHeader("GPO", selectedTab); - Add(@" -
-
-
-
"); - - GenerateSectionFluid("Active Directory Indicators", GenerateIndicators, selectedTab, true); - GenerateSectionFluid("Rules Matched", GenerateRulesMatched, selectedTab); - GenerateSectionFluid("Domain Information", GenerateDomainInformation, selectedTab); - GenerateSectionFluid("User Information", GenerateUserInformation, selectedTab); - GenerateSectionFluid("Computer Information", GenerateComputerInformation, selectedTab); - GenerateSectionFluid("Admin Groups", GenerateAdminGroupsInformation, selectedTab); - GenerateSectionFluid("Trusts", GenerateTrustInformation, selectedTab); - GenerateSectionFluid("Anomalies", GenerateAnomalyDetail, selectedTab); - GenerateSectionFluid("Password Policies", GeneratePasswordPoliciesDetail, selectedTab); - GenerateSectionFluid("GPO", GenerateGPODetail, selectedTab); - - Add(@" -
-
-
"); - return sb.ToString(); ; - } - - private void GenerateTabHeader(string title, string selectedTab, bool defaultIfTabEmpty = false) - { - string id = GenerateId(title); - bool isActive = (String.IsNullOrEmpty(selectedTab) ? defaultIfTabEmpty : selectedTab == id); - Add(@""); - Add(title); - Add(""); - } - - private void GenerateRulesMatched() - { - Add(@" -
-
- - - - - - - - - - - -"); - foreach (HealthcheckData data in consolidation) - { - foreach (HealthcheckRiskRule rule in data.RiskRules) - { - Add(@" - - - - - - - - "); - } - } - Add(@" - -
DomainCategoryRuleScoreDescriptionRationale
" + PrintDomain(data.Domain) + @"" + rule.Category + @"" + rule.RiskId + @"" + rule.Points + @"" + HealthcheckRules.GetRuleDescription(rule.RiskId) + @"" + rule.Rationale + @"
-
-
"); - } - - #region indicators - - private void GenerateIndicators() - { - int globalScore = 0, minscore = 0, maxscore = 0, medianscore = 0; - int sumScore = 0, num = 0; - List AllScores = new List(); - foreach (var data in consolidation) - { - num++; - sumScore += data.GlobalScore; - AllScores.Add(data.GlobalScore); - } - if (num > 0) - { - AllScores.Sort(); - - globalScore = sumScore / num; - minscore = AllScores[0]; - maxscore = AllScores[AllScores.Count - 1]; - if (AllScores.Count % 2 == 0) - { - var firstValue = AllScores[(AllScores.Count / 2) - 1]; - var secondValue = AllScores[(AllScores.Count / 2)]; - medianscore = (firstValue + secondValue) / 2; - } - if (AllScores.Count % 2 == 1) - { - medianscore = AllScores[(AllScores.Count / 2)]; - } - } - Add(@" - -
-
-
"); - GenerateGauge(globalScore); - Add(@"
-
-
-

Average Risk Level: " + globalScore + @" / 100

-

Best Risk Level: " + minscore + @" / 100

-

Worst Risk Level: " + maxscore + @" / 100

-

Median Risk Level: " + medianscore + @" / 100

-
-
-"); - GenerateRiskModelPanel(); - GenerateIndicatorsTable(); - } - - void GenerateRiskModelPanel() - { - Add(@" - -
-
- - - -"); - var riskmodel = new Dictionary>(); - foreach (HealthcheckRiskRuleCategory category in Enum.GetValues(typeof(HealthcheckRiskRuleCategory))) - { - riskmodel[category] = new List(); - } - for (int j = 0; j < 4; j++) - { - for (int i = 0; ; i++) - { - int id = (1000 * j + 1000 + i); - if (Enum.IsDefined(typeof(HealthcheckRiskModelCategory), id)) - { - riskmodel[(HealthcheckRiskRuleCategory)j].Add((HealthcheckRiskModelCategory)id); - } - else - break; - } - } - foreach (HealthcheckRiskRuleCategory category in Enum.GetValues(typeof(HealthcheckRiskRuleCategory))) - { - riskmodel[category].Sort( - (HealthcheckRiskModelCategory a, HealthcheckRiskModelCategory b) => - { - return GetEnumDescription(a).CompareTo(GetEnumDescription(b)); - }); - } - for (int i = 0; ; i++) - { - string line = ""; - bool HasValue = false; - foreach (HealthcheckRiskRuleCategory category in Enum.GetValues(typeof(HealthcheckRiskRuleCategory))) - { - if (i < riskmodel[category].Count) - { - HasValue = true; - HealthcheckRiskModelCategory model = riskmodel[category][i]; - int score = 0; - int numrules = 0; - int numImpacted = 0; - List rulematched = new List(); - foreach (HealthcheckData data in consolidation) - { - bool impactedDomain = false; - foreach (HealthcheckRiskRule rule in data.RiskRules) - { - if (rule.Model == model) - { - impactedDomain = true; - numrules++; - score += rule.Points; - rulematched.Add(rule); - } - } - if (impactedDomain) - numImpacted++; - } - string tdclass = ""; - if (numrules == 0) - { - tdclass = "model_good"; - } - else if (score == 0) - { - tdclass = "model_info"; - } - else if (score <= 10 * consolidation.Count) - { - tdclass = "model_toimprove"; - } - else if (score <= 30 * consolidation.Count) - { - tdclass = "model_warning"; - } - else - { - tdclass = "model_danger"; - } - string tooltip = (numImpacted > 0 ? " Impacted domains: " + numImpacted + " Rules: " + numrules + " Average Score: " + (score / consolidation.Count) : "No domain impacted"); - string tooltipdetail = null; - string modelstring = GetEnumDescription(model); - rulematched.Sort((HealthcheckRiskRule a, HealthcheckRiskRule b) - => - { - return a.Points.CompareTo(b.Points); - }); - foreach (var rule in rulematched) - { - tooltipdetail += rule.Rationale + "
"; - } - line += "
"; - } - else - line += ""; - } - line += ""; - if (HasValue) - Add(line); - else - break; - } - Add(@" - -
Staled ObjectsPrivileged accountsTrustsAnomalies
" + tooltipdetail + "

") + "\">" + modelstring + "
-
-
- Legend:
-   score is 0 - no risk identified but some improvements detected
-   score between 1 and 10 - a few actions have been identified
-   score between 10 and 30 - rules should be looked with attention
-   score higher than 30 - major risks identified -
-
"); - } - - private void GenerateIndicatorsTable() - { - Add(@" - -
-
- - - - - - - - - - - - -"); - foreach (HealthcheckData data in consolidation) - { - Add(@" - - - - - - - - - "); - } - Add(@" - -
DomainDomain Risk LevelStale objectsPrivileged accountsTrustsAnomaliesGenerated
" + PrintDomain(data.Domain) + @"" + data.GlobalScore + @"" + data.StaleObjectsScore + @"" + data.PrivilegiedGroupScore + @"" + data.TrustScore + @"" + data.AnomalyScore + @"" + data.GenerationDate.ToString("u") + @"
-
-
-"); - } - #endregion indicators - - #region domain information - private void GenerateDomainInformation() - { - Add(@" -
-
- - - - - - - - - - - - - -"); - foreach (HealthcheckData data in consolidation) - { - Add(@" - - - - - - - - - - "); - } - Add(@" - - - - - - - -
DomainNetbios NameDomain Functional LevelForest Functional LevelCreation dateNb DCEngineLevel
" + PrintDomain(data.Domain) + @"" + Encode(data.NetBIOSName) + @"" + DecodeDomainFunctionalLevel(data.DomainFunctionalLevel) + @"" + DecodeForestFunctionalLevel(data.ForestFunctionalLevel) + @"" + data.DomainCreation.ToString("u") + @"" + data.NumberOfDC + @"" + data.EngineVersion + @"" + data.Level + @"
Total" + consolidation.Count + @"
-
-
-"); - } - #endregion domain information - - #region user - private void GenerateUserInformation() - { - Add(@" -
-
- - - - - - - - - - - - - - - - - - - -"); - HealthcheckAccountData total = new HealthcheckAccountData(); - foreach (HealthcheckData data in consolidation) - { - total.Add(data.UserAccountData); - Add(@" - - - - - - - - - - - - - - - - "); - } - Add(@" - - - - - - - - - - - - - - - - - - - -
DomainNb User AccountsNb EnabledNb DisabledNb ActiveNb InactiveNb LockedNb pwd never ExpireNb SidHistoryNb Bad PrimaryGroupNb Password not Req.Nb Des enabled.Nb Trusted delegationNb Reversible password
" + PrintDomain(data.Domain) + @"" + data.UserAccountData.Number + @"" + data.UserAccountData.NumberEnabled + @"" + data.UserAccountData.NumberDisabled + @"" + data.UserAccountData.NumberActive + @"" + data.UserAccountData.NumberInactive + @"" + data.UserAccountData.NumberLocked + @"" + data.UserAccountData.NumberPwdNeverExpires + @"" + data.UserAccountData.NumberSidHistory + @"" + data.UserAccountData.NumberBadPrimaryGroup + @"" + data.UserAccountData.NumberPwdNotRequired + @"" + data.UserAccountData.NumberDesEnabled + @"" + data.UserAccountData.NumberTrustedToAuthenticateForDelegation + @"" + data.UserAccountData.NumberReversibleEncryption + @"
Total" + total.Number + @"" + total.NumberEnabled + @"" + total.NumberDisabled + @"" + total.NumberActive + @"" + total.NumberInactive + @"" + total.NumberLocked + @"" + total.NumberPwdNeverExpires + @"" + total.NumberSidHistory + @"" + total.NumberBadPrimaryGroup + @"" + total.NumberPwdNotRequired + @"" + total.NumberDesEnabled + @"" + total.NumberTrustedToAuthenticateForDelegation + @"" + total.NumberReversibleEncryption + @"
-
-
-"); - } - #endregion user - - #region computer - private void GenerateComputerInformation() - { - Add(@" -
-
- - - - - - - - - - - - - - - -"); - HealthcheckAccountData total = new HealthcheckAccountData(); - foreach (HealthcheckData data in consolidation) - { - total.Add(data.ComputerAccountData); - Add(@" - - - - - - - - - - - - "); - } - Add(@" - - - - - - - - - - - - - - - -
DomainNb Computer AccountsNb EnabledNb DisabledNb ActiveNb InactiveNb SidHistoryNb Bad PrimaryGroupNb Trusted delegationNb Reversible password
" + PrintDomain(data.Domain) + @"" + data.ComputerAccountData.Number + @"" + data.ComputerAccountData.NumberEnabled + @"" + data.ComputerAccountData.NumberDisabled + @"" + data.ComputerAccountData.NumberActive + @"" + data.ComputerAccountData.NumberInactive + @"" + data.ComputerAccountData.NumberSidHistory + @"" + data.ComputerAccountData.NumberBadPrimaryGroup + @"" + data.ComputerAccountData.NumberTrustedToAuthenticateForDelegation + @"" + data.ComputerAccountData.NumberReversibleEncryption + @"
Total" + total.Number + @"" + total.NumberEnabled + @"" + total.NumberDisabled + @"" + total.NumberActive + @"" + total.NumberInactive + @"" + total.NumberSidHistory + @"" + total.NumberBadPrimaryGroup + @"" + total.NumberTrustedToAuthenticateForDelegation + @"" + total.NumberReversibleEncryption + @"
-
-
-"); - GenerateConsolidatedOperatingSystemList(); - } - - private string GenerateConsolidatedOperatingSystemList() - { - string output = null; - List AllOS = new List(); - Dictionary SpecificOK = new Dictionary(); - foreach (HealthcheckData data in consolidation) - { - if (data.OperatingSystem != null) - { - foreach (HealthcheckOSData os in data.OperatingSystem) - { - // keep only the "good" operating system (OsToInt>0) - if (OSToInt(os.OperatingSystem) > 0) - { - if (!AllOS.Contains(os.OperatingSystem)) - AllOS.Add(os.OperatingSystem); - } - else - { - // consolidate all other OS - if (!SpecificOK.ContainsKey(os.OperatingSystem)) - SpecificOK[os.OperatingSystem] = os.NumberOfOccurence; - else - SpecificOK[os.OperatingSystem] += os.NumberOfOccurence; - } - } - } - } - AllOS.Sort(OrderOS); - Add(@" -
-
- - - -"); - foreach (string os in AllOS) - { - Add("\r\n"); - } - Add(@" - - - -"); - // maybe not the most perfomant algorithm (n^4) but there is only a few domains to consolidate - foreach (HealthcheckData data in consolidation) - { - Add(@" - -"); - foreach (string os in AllOS) - { - int numberOfOccurence = -1; - if (data.OperatingSystem != null) - { - foreach (var OS in data.OperatingSystem) - { - if (OS.OperatingSystem == os) - { - numberOfOccurence = OS.NumberOfOccurence; - break; - } - } - } - Add("\r\n"); - } - Add("\r\n"); - } - Add(@" - - - - - -"); - foreach (string os in AllOS) - { - int total = 0; - foreach (HealthcheckData data in consolidation) - { - if (data.OperatingSystem != null) - { - foreach (var OS in data.OperatingSystem) - { - if (OS.OperatingSystem == os) - { - total += OS.NumberOfOccurence; - break; - } - } - } - } - Add(@""); - } - Add(@" - -
Domain" + Encode(os) + "
" + PrintDomain(data.Domain) + @"" + (numberOfOccurence >= 0 ? numberOfOccurence.ToString() : null) + "
Total" + total + "
-
-
"); - if (SpecificOK.Count > 0) - { - Add(@" -
-
- - - - - - - "); - foreach (string os in SpecificOK.Keys) - { - Add(""); - } - Add(@" - -
Operating SystemNb
Nb " + Encode(os) + " : " + SpecificOK[os] + "
-
-
"); - } - return output; - } - #endregion computer - - #region admin - private void GenerateAdminGroupsInformation() - { - Add(@" -
-
- - - - - - - - - - - - - - -"); - foreach (HealthcheckData data in consolidation) - { - foreach (HealthCheckGroupData group in data.PrivilegedGroups) - { - Add(@" - - - - - - - - - - - -"); - } - } - Add(@" - -
DomainGroup NameNb AdminsNb EnabledNb DisabledNb InactiveNb PWd never expireNb can be delegatedNb external users
" + PrintDomain(data.Domain) + @"" + Encode(group.GroupName) + @"" + group.NumberOfMember + @"" + group.NumberOfMemberEnabled + @"" + group.NumberOfMemberDisabled + @"" + group.NumberOfMemberInactive + @"" + group.NumberOfMemberPwdNeverExpires + @"" + group.NumberOfMemberCanBeDelegated + @"" + group.NumberOfExternalMember + @"
-
-
-"); - } - #endregion admin - - #region trust - private void GenerateTrustInformation() - { - List knowndomains = new List(); - GenerateSubSection("Discovered domains"); - Add(@" -
-
- - - - - - - - - - - - - -"); - foreach (HealthcheckData data in consolidation) - { - - if (!knowndomains.Contains(data.DomainFQDN)) - knowndomains.Add(data.DomainFQDN); - data.Trusts.Sort( - (HealthCheckTrustData a, HealthCheckTrustData b) - => - { - return String.Compare(a.TrustPartner, b.TrustPartner); - } - ); - - foreach (HealthCheckTrustData trust in data.Trusts) - { - if (!knowndomains.Contains(trust.TrustPartner)) - knowndomains.Add(trust.TrustPartner); - Add(@" - - - - - - - - - - -"); - } - } - Add(@" - -
DomainTrust PartnerTypeAttributDirectionSID Filtering activeCreationIs Active ?
" + PrintDomain(data.Domain) + @"" + PrintDomain(trust.Domain) + @"" + TrustAnalyzer.GetTrustType(trust.TrustType) + @"" + TrustAnalyzer.GetTrustAttribute(trust.TrustAttributes) + @"" + TrustAnalyzer.GetTrustDirection(trust.TrustDirection) + @"" + TrustAnalyzer.GetSIDFiltering(trust) + @"" + trust.CreationDate.ToString("u") + @"" + trust.IsActive + @"
-
-
-"); - GenerateSubSection("Other discovered domains"); - Add(@" -
-
- - - - - - - - - - -"); - foreach (HealthcheckData data in consolidation) - { - foreach (HealthCheckTrustData trust in data.Trusts) - { - if (trust.KnownDomains == null) - continue; - trust.KnownDomains.Sort((HealthCheckTrustDomainInfoData a, HealthCheckTrustDomainInfoData b) - => - { - return String.Compare(a.DnsName, b.DnsName); - } - ); - foreach (HealthCheckTrustDomainInfoData di in trust.KnownDomains) - { - if (knowndomains.Contains(di.DnsName)) - continue; - knowndomains.Add(di.DnsName); - Add(@" - - - - - - - -"); - } - } - } - foreach (HealthcheckData data in consolidation) - { - if (data.ReachableDomains != null) - { - foreach (HealthCheckTrustDomainInfoData di in data.ReachableDomains) - { - if (knowndomains.Contains(di.DnsName)) - continue; - knowndomains.Add(di.DnsName); - Add(@" - - - - - - - -"); - } - } - } - - Add(@" - -
FromReachable domainViaNetbiosCreation date
" + PrintDomain(data.Domain) + @"" + Encode(di.DnsName) + @"" + Encode(trust.TrustPartner) + @"" + Encode(di.NetbiosName) + @"" + di.CreationDate.ToString("u") + @"
" + PrintDomain(data.Domain) + @"" + Encode(di.DnsName) + @"Unknown" + Encode(di.NetbiosName) + @"Unknown
-
-
-"); - - // prepare a SID map to locate unknown account - SortedDictionary sidmap = new SortedDictionary(); - GenerateSubSection("SID Map"); - Add(@" -
-
- - - - - - - -"); - foreach (HealthcheckData data in consolidation) - { - if (!sidmap.ContainsKey(data.DomainFQDN) && !String.IsNullOrEmpty(data.DomainSid)) - { - sidmap.Add(data.DomainFQDN, data.DomainSid); - } - foreach (HealthCheckTrustData trust in data.Trusts) - { - if (!sidmap.ContainsKey(trust.TrustPartner) && !String.IsNullOrEmpty(trust.SID)) - { - sidmap.Add(trust.TrustPartner, trust.SID); - } - foreach (HealthCheckTrustDomainInfoData di in trust.KnownDomains) - { - if (!sidmap.ContainsKey(di.DnsName) && !String.IsNullOrEmpty(di.Sid)) - { - sidmap.Add(di.DnsName, di.Sid); - } - } - } - - } - foreach (HealthcheckData data in consolidation) - { - if (data.ReachableDomains != null) - { - foreach (HealthCheckTrustDomainInfoData di in data.ReachableDomains) - { - if (!sidmap.ContainsKey(di.DnsName) && !String.IsNullOrEmpty(di.Sid)) - { - sidmap.Add(di.DnsName, di.Sid); - } - } - } - } - foreach (string domain in sidmap.Keys) - { - Add(@" - - - - -"); - } - Add(@" - -
DomainDomain SID
" + Encode(domain) + @"" + sidmap[domain] + @"
-
-
-"); - } - #endregion trust - - #region anomaly - private void GenerateAnomalyDetail() - { - Add(@" -
-
- - - - - - - - - - - -"); - foreach (HealthcheckData data in consolidation) - { - Add(@" - - - - - - - - -"); - } - Add(@" - -
DomainKrbtgtAdminSDHolderDC with null sessionSmart card account not updateDate LAPS Installed
" + PrintDomain(data.Domain) + @"" + data.KrbtgtLastChangeDate.ToString("u") + @"" + data.AdminSDHolderNotOKCount + @"" + data.DomainControllerWithNullSessionCount + @"" + data.SmartCardNotOKCount + @"" + (data.LAPSInstalled == DateTime.MaxValue ? "Never" : (data.LAPSInstalled == DateTime.MinValue ? "Not checked" : data.LAPSInstalled.ToString("u"))) + @"
-
-
-"); - } - #endregion anomaly - - #region passwordpolicy - private void GeneratePasswordPoliciesDetail() - { - GenerateSubSection("Password policies"); - Add(@" -
-
- - - - - - - - - - - - - - - - -"); - foreach (HealthcheckData data in consolidation) - { - if (data.GPPPasswordPolicy != null) - { - foreach (GPPSecurityPolicy policy in data.GPPPasswordPolicy) - { - Add(@" - - - - - - - - - - - - - -"); - } - } - } - Add(@" - -
DomainPolicy NameComplexityMax Password AgeMin Password AgeMin Password LengthPassword HistoryReversible EncryptionLockout ThresholdLockout DurationReset account counter locker after
" + PrintDomain(data.Domain) + @"" + Encode(policy.GPOName) + @"" + GetPSOStringValue(policy, "PasswordComplexity") + @"" + GetPSOStringValue(policy, "MaximumPasswordAge") + @"" + GetPSOStringValue(policy, "MinimumPasswordAge") + @"" + GetPSOStringValue(policy, "MinimumPasswordLength") + @"" + GetPSOStringValue(policy, "PasswordHistorySize") + @"" + GetPSOStringValue(policy, "ClearTextPassword") + @"" + GetPSOStringValue(policy, "LockoutBadCount") + @"" + GetPSOStringValue(policy, "LockoutDuration") + @"" + GetPSOStringValue(policy, "ResetLockoutCount") + @"
-
-
-"); - GenerateSubSection("Screensaver policies"); - Add(@" -
-
- - - - - - - - - - - -"); - foreach (HealthcheckData data in consolidation) - { - if (data.GPPPasswordPolicy != null) - { - foreach (GPPSecurityPolicy policy in data.GPOScreenSaverPolicy) - { - string scrActive = GetPSOStringValue(policy, "ScreenSaveActive"); - string scrSecure = GetPSOStringValue(policy, "ScreenSaverIsSecure"); - string scrTimeOut = GetPSOStringValue(policy, "ScreenSaveTimeOut"); - string scrGrace = GetPSOStringValue(policy, "ScreenSaverGracePeriod"); - - Add(@" - - - - - - - - -"); - } - } - } - Add(@" - -
DomainPolicy NameScreensaver enforcedPassword requestStart after (seconds)Grace Period (seconds)
" + PrintDomain(data.Domain) + @"" + Encode(policy.GPOName) + @"" + scrActive + @"" + scrSecure + @"" + scrTimeOut + @"" + scrGrace + @"
-
-
-"); - GenerateSubSection("LSA settings"); - Add(@" -
-
- - - - - - - - "); - foreach (HealthcheckData data in consolidation) - { - if (data.GPOLsaPolicy != null) - { - foreach (GPPSecurityPolicy policy in data.GPOLsaPolicy) - { - foreach (GPPSecurityPolicyProperty property in policy.Properties) - { - Add(@" - - - - - - -"); - } - } - } - } - Add(@" - -
DomainPolicy NameSettingValue
" + PrintDomain(data.Domain) + @"" + Encode(policy.GPOName) + @"" + GetLinkForLsaSetting(property.Property) + @"" + property.Value + @"
-
-
-"); - } - #endregion passwordpolicy - - #region gpo detail - private void GenerateGPODetail() - { - GenerateSubSection("Obfuscated Password"); - Add(@" -
-
- - - - - - - - - - - - -"); - foreach (HealthcheckData data in consolidation) - { - foreach (GPPPassword password in data.GPPPassword) - { - Add(@" - - - - - - - - - -"); - } - } - Add(@" - -
DomainGPO NamePassword originUserNamePasswordChangedOther
" + PrintDomain(data.Domain) + @"" + Encode(password.GPOName) + @"" + Encode(password.Type) + @"" + Encode(password.UserName) + @"" + Encode(password.Password) + @"" + password.Changed.ToString("u") + @"" + Encode(password.Other) + @"
-
-
-"); - } - #endregion gpo detail - - new string PrintDomain(DomainKey key) - { - string label = PrintDomainLabel(key); - if (GetUrlCallback == null) - return label; - string htmlData = GetUrlCallback(key, label); - if (String.IsNullOrEmpty(htmlData)) - return label; - return htmlData; - } - - string PrintDomainLabel(DomainKey key) - { - if (HasDomainAmbigousName != null) - { - if (HasDomainAmbigousName(key)) - return key.ToString(); - } - else if (consolidation.HasDomainAmbigiousName(key)) - return key.ToString(); - return key.DomainName; - } - } -} diff --git a/Report/HealthCheckReportSingle.cs b/Report/HealthCheckReportSingle.cs deleted file mode 100644 index be47e51..0000000 --- a/Report/HealthCheckReportSingle.cs +++ /dev/null @@ -1,2351 +0,0 @@ -// -// Copyright (c) Ping Castle. All rights reserved. -// https://www.pingcastle.com -// -// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. -// -using PingCastle.template; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Globalization; -using System.IO; -using System.Reflection; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using System.Text; - -namespace PingCastle.Healthcheck -{ - public class HealthCheckReportSingle : HealthCheckReportBase - { - - private readonly HealthcheckData HealthCheckData; - public static int MaxNumberUsersInHtmlReport = 100; - private readonly ADHealthCheckingLicense _license; - private readonly Version version; - - public HealthCheckReportSingle(HealthcheckData healthcheckData, ADHealthCheckingLicense license) - { - HealthCheckData = healthcheckData; - version = new Version(HealthCheckData.EngineVersion.Split(' ')[0]); - _license = license; - } - - protected override string GenerateTitleInformation() - { - return Encode(HealthCheckData.DomainFQDN + " PingCastle " + HealthCheckData.GenerationDate.ToString("yyyy-MM-dd")); - } - - - protected override string GenerateHeaderInformation() - { - return GetStyleSheetTheme() + GetStyleSheet(); - } - - public static string GetStyleSheet() - { - return @" - - -"; - } - - protected override string GenerateBodyInformation() - { - sb = new StringBuilder(); - Add(@" - - -
-
- -
-
-

About

-
-
-
-
-

Generated by Ping Castle all rights reserved

-

Open source components:

- -
-
-
-
- -
-
- -
-
-
- -

"); - Add(HealthCheckData.DomainFQDN); - Add(@" - Healthcheck analysis

-

Date: "); - Add(HealthCheckData.GenerationDate.ToString("yyyy-MM-dd")); - Add(@" - Engine version: "); - Add(HealthCheckData.EngineVersion); - Add(@"

-"); - Add(@"
-This report has been generated with the "); - Add(String.IsNullOrEmpty(_license.Edition) ? "Basic" : _license.Edition); - Add(@" Edition of PingCastle."); - if (String.IsNullOrEmpty(_license.Edition)) - { - Add(@" -
Being part of a commercial package is forbidden (selling the information contained in the report).
-If you are an auditor, you MUST purchase an Auditor license to share the development effort. -"); - } - Add(@"
-"); - Add(@"
-"); - GenerateContent(); - Add(@" -
-"); - return sb.ToString(); - } - - public void GenerateContent() - { - GenerateSection("Active Directory Indicators", GenerateIndicators); - GenerateSection("Domain Information", GenerateDomainInformation); - GenerateSection("User Information", GenerateUserInformation); - GenerateSection("Computer Information", GenerateComputerInformation); - GenerateSection("Admin Groups", GenerateAdminGroupsInformation); - GenerateSection("Trusts", GenerateTrustInformation); - GenerateSection("Anomalies", GenerateAnomalyDetail); - GenerateSection("Password Policies", GeneratePasswordPoliciesDetail); - GenerateSection("GPO", GenerateGPODetail); - } - - protected override string GenerateFooterInformation() - { - return @" - -"; - } - - #region indicators - private void GenerateIndicators() - { - GenerateSubSection("Indicators"); - Add(@" -
-
-
"); - GenerateGauge(HealthCheckData.GlobalScore); - Add(@"
-
-
-

Domain Risk Level: "); - Add(HealthCheckData.GlobalScore.ToString()); - Add(@" / 100

-

It is the maximum score of the 4 indicators and one score cannot be higher than 100. The lower the better

-
-
-
-"); - GenerateSubIndicator("Stale Object", HealthCheckData.StaleObjectsScore, HealthcheckRiskRuleCategory.StaleObjects, "It is about operations related to user or computer objects", "DetailStale"); - GenerateSubIndicator("Trusts", HealthCheckData.TrustScore, HealthcheckRiskRuleCategory.Trusts, "It is about links between two Active Directories", "DetailTrusts"); - GenerateSubIndicator("Privileged Accounts", HealthCheckData.PrivilegiedGroupScore, HealthcheckRiskRuleCategory.PrivilegedAccounts, "It is about administrators of the Active Directory", "DetailPrivileged"); - GenerateSubIndicator("Anomalies", HealthCheckData.AnomalyScore, HealthcheckRiskRuleCategory.Anomalies, "It is about specific security control points", "DetailAnomalies"); - Add(@" -
-"); - GenerateRiskModelPanel(); - GenerateIndicatorPanel("DetailStale", "Stale Objects rule details", HealthcheckRiskRuleCategory.StaleObjects); - GenerateIndicatorPanel("DetailTrusts", "Trusts rule details", HealthcheckRiskRuleCategory.Trusts); - GenerateIndicatorPanel("DetailPrivileged", "Privileged Accounts rule details", HealthcheckRiskRuleCategory.PrivilegedAccounts); - GenerateIndicatorPanel("DetailAnomalies", "Anomalies rule details", HealthcheckRiskRuleCategory.Anomalies); - } - - void GenerateSubIndicator(string category, int score, HealthcheckRiskRuleCategory RiskRuleCategory, string explanation, string reference) - { - int numrules = 0; - if (HealthCheckData.RiskRules != null) - { - foreach (var rule in HealthCheckData.RiskRules) - { - if (rule.Category == RiskRuleCategory) - numrules++; - } - } - Add(@" -
-
-
-
"); - GenerateGauge(score); - Add(@"
-
-
- "); - Add((score == HealthCheckData.GlobalScore ? "" : "")); - Add(@"

"); - Add(category); - Add(@" : "); - Add(score.ToString()); - Add(@" /100

"); - Add((score == HealthCheckData.GlobalScore ? "
" : "")); - Add(@" -

"); - Add(Encode(explanation)); - Add(@"

-
-
-

"); - Add(numrules.ToString()); - Add(@" rules matched

-
-
-
-"); - } - - int GetRulesNumberForCategory(HealthcheckRiskRuleCategory category) - { - int count = 0; - foreach (var rule in HealthCheckData.RiskRules) - { - if (rule.Category == category) - count++; - } - return count; - } - - void GenerateRiskModelPanel() - { - Add(@" - -
-
- - - -"); - var riskmodel = new Dictionary>(); - foreach (HealthcheckRiskRuleCategory category in Enum.GetValues(typeof(HealthcheckRiskRuleCategory))) - { - riskmodel[category] = new List(); - } - for (int j = 0; j < 4; j++) - { - for (int i = 0; ; i++) - { - int id = (1000 * j + 1000 + i); - if (Enum.IsDefined(typeof(HealthcheckRiskModelCategory), id)) - { - riskmodel[(HealthcheckRiskRuleCategory)j].Add((HealthcheckRiskModelCategory)id); - } - else - break; - } - } - foreach (HealthcheckRiskRuleCategory category in Enum.GetValues(typeof(HealthcheckRiskRuleCategory))) - { - riskmodel[category].Sort( - (HealthcheckRiskModelCategory a, HealthcheckRiskModelCategory b) => - { - return GetEnumDescription(a).CompareTo(GetEnumDescription(b)); - }); - } - for (int i = 0;; i++) - { - string line = ""; - bool HasValue = false; - foreach (HealthcheckRiskRuleCategory category in Enum.GetValues(typeof(HealthcheckRiskRuleCategory))) - { - if (i < riskmodel[category].Count) - { - HasValue = true; - HealthcheckRiskModelCategory model = riskmodel[category][i]; - int score = 0; - int numrules = 0; - List rulematched = new List(); - foreach (HealthcheckRiskRule rule in HealthCheckData.RiskRules) - { - if (rule.Model == model) - { - numrules++; - score += rule.Points; - rulematched.Add(rule); - } - } - string tdclass = ""; - if (numrules == 0) - { - tdclass = "model_good"; - } - else if (score == 0) - { - tdclass = "model_info"; - } - else if (score <= 10) - { - tdclass = "model_toimprove"; - } - else if (score <= 30) - { - tdclass = "model_warning"; - } - else - { - tdclass = "model_danger"; - } - string tooltip = "Rules: " + numrules + " Score: " + score; - string tooltipdetail = null; - string modelstring = GetEnumDescription(model); - rulematched.Sort((HealthcheckRiskRule a, HealthcheckRiskRule b) - => - { - return a.Points.CompareTo(b.Points); - }); - foreach (var rule in rulematched) - { - tooltipdetail += Encode(rule.Rationale) + "
"; - } - line += "
"; - } - else - line += ""; - } - line += ""; - if (HasValue) - Add(line); - else - break; - } - Add(@" - -
Staled ObjectsPrivileged accountsTrustsAnomalies
" + tooltipdetail + "

") + "\">" + modelstring + "
-
-
- Legend:
-   score is 0 - no risk identified but some improvements detected
-   score between 1 and 10 - a few actions have been identified
-   score between 10 and 30 - rules should be looked with attention
-   score higher than 30 - major risks identified -
-
"); - } - - void GenerateIndicatorPanel(string id, string title, HealthcheckRiskRuleCategory category) - { - Add(@" - -
-
-"); - HealthCheckData.RiskRules.Sort((HealthcheckRiskRule a, HealthcheckRiskRule b) - => - {return -a.Points.CompareTo(b.Points); - } - ); - foreach (HealthcheckRiskRule rule in HealthCheckData.RiskRules) - { - if (rule.Category == category) - GenerateIndicatorPanelDetail(category, rule); - } - Add(@" -
-
"); - } - - string NewLineToBR(string data) - { - if (String.IsNullOrEmpty(data)) - return data; - return data.Replace("\r\n", "
\r\n"); - } - - void GenerateIndicatorPanelDetail(HealthcheckRiskRuleCategory category, HealthcheckRiskRule rule) - { - string safeRuleId = rule.RiskId.Replace("$", "dollar"); - Add(@" -
- -
-"); - Add(@" -
-"); - var hcrule = HealthcheckRules.GetRuleFromID(rule.RiskId); - if (hcrule == null) - { - } - else - { - Add("

"); - Add(hcrule.Title); - Add("

\r\nDescription:

"); - Add(NewLineToBR(hcrule.Description)); - Add("

\r\nTechnical explanation:

"); - Add(NewLineToBR(hcrule.TechnicalExplanation)); - Add("

\r\nAdvised solution:

"); - Add(NewLineToBR(hcrule.Solution)); - Add("

\r\nPoints:

"); - Add(NewLineToBR(hcrule.GetComputationModelString())); - Add("

\r\n"); - if (!String.IsNullOrEmpty(hcrule.Documentation)) - { - Add("Documentation:

"); - Add(hcrule.Documentation); - Add("

"); - } - } - if (rule.Details != null && rule.Details.Count > 0) - { - Add("Details:

"); - Add(String.Join("
\r\n", rule.Details.ToArray())); - Add("

"); - } - Add(@" -
-
-
"); - } - - #endregion indicators - - #region domain info - private void GenerateDomainInformation() - { - Add(@" -
-
- - - - - - - - - - - - - - - - - - - - - - - - -
DomainNetbios NameDomain Functional LevelForest Functional LevelCreation dateDC countSchema version
"); - Add(HealthCheckData.DomainFQDN); - Add(@""); - Add(HealthCheckData.NetBIOSName); - Add(@""); - Add(DecodeDomainFunctionalLevel(HealthCheckData.DomainFunctionalLevel)); - Add(@""); - Add(DecodeForestFunctionalLevel(HealthCheckData.ForestFunctionalLevel)); - Add(@""); - Add(HealthCheckData.DomainCreation); - Add(@""); - Add(HealthCheckData.NumberOfDC); - Add(@""); - Add(HealthCheckData.SchemaVersion); - Add(@"
-
-
-"); - } - - #endregion domain info - - #region user info - private void GenerateUserInformation() - { - Add(@" -
-
- - - - - - - - - - - - - - - - - - - - - - -
Nb User AccountsNb EnabledNb DisabledNb ActiveNb InactiveNb LockedNb pwd never ExpireNb SidHistoryNb Bad PrimaryGroupNb Password not Req.Nb Des enabled.Nb Trusted delegationNb Reversible password
"); - Add(HealthCheckData.UserAccountData.Number); - Add(@""); - Add(HealthCheckData.UserAccountData.NumberEnabled); - Add(@""); - Add(HealthCheckData.UserAccountData.NumberDisabled); - Add(@""); - Add(HealthCheckData.UserAccountData.NumberActive); - Add(@""); - SectionList("usersaccordion", "sectioninactiveuser", HealthCheckData.UserAccountData.NumberInactive, HealthCheckData.UserAccountData.ListInactive); - Add(@""); - SectionList("usersaccordion", "sectionlockeduser", HealthCheckData.UserAccountData.NumberLocked, HealthCheckData.UserAccountData.ListLocked); - Add(@""); - SectionList("usersaccordion", "sectionneverexpiresuser", HealthCheckData.UserAccountData.NumberPwdNeverExpires, HealthCheckData.UserAccountData.ListPwdNeverExpires); - Add(@""); - SectionList("usersaccordion", "sectionsidhistoryuser", HealthCheckData.UserAccountData.NumberSidHistory, HealthCheckData.UserAccountData.ListSidHistory); - Add(@""); - SectionList("usersaccordion", "sectionbadprimarygroupuser", HealthCheckData.UserAccountData.NumberBadPrimaryGroup, HealthCheckData.UserAccountData.ListBadPrimaryGroup); - Add(@""); - SectionList("usersaccordion", "sectionpwdnotrequireduser", HealthCheckData.UserAccountData.NumberPwdNotRequired, HealthCheckData.UserAccountData.ListPwdNotRequired); - Add(@""); - SectionList("usersaccordion", "sectiondesenableduser", HealthCheckData.UserAccountData.NumberDesEnabled, HealthCheckData.UserAccountData.ListDesEnabled); - Add(@""); - SectionList("usersaccordion", "sectiontrusteddelegationuser", HealthCheckData.UserAccountData.NumberTrustedToAuthenticateForDelegation, HealthCheckData.UserAccountData.ListTrustedToAuthenticateForDelegation); - Add(@""); - SectionList("usersaccordion", "sectionreversiblenuser", HealthCheckData.UserAccountData.NumberReversibleEncryption, HealthCheckData.UserAccountData.ListReversibleEncryption); - Add(@"
-
-
-"); - GenerateListAccount(HealthCheckData.UserAccountData, "user", "usersaccordion"); - GenerateDomainSIDHistoryList(HealthCheckData.UserAccountData); - } - - private void GenerateListAccount(HealthcheckAccountData data, string root, string accordion) - { - GenerateAccordion(accordion, - () => - { - if (data.ListInactive != null && data.ListInactive.Count > 0) - { - GenerateListAccountDetail(accordion, "sectioninactive" + root, "Inactive objects (Last usage > 6 months) ", data.ListInactive); - } - if (data.ListLocked != null && data.ListLocked.Count > 0) - { - GenerateListAccountDetail(accordion, "sectionlocked" + root, "Locked objects ", data.ListLocked); - } - if (data.ListPwdNeverExpires != null && data.ListPwdNeverExpires.Count > 0) - { - GenerateListAccountDetail(accordion, "sectionneverexpires" + root, "Objects with a password which never expires ", data.ListPwdNeverExpires); - } - if (data.ListSidHistory != null && data.ListSidHistory.Count > 0) - { - GenerateListAccountDetail(accordion, "sectionsidhistory" + root, "Objects having the SIDHistory populated ", data.ListSidHistory); - } - if (data.ListBadPrimaryGroup != null && data.ListBadPrimaryGroup.Count > 0) - { - GenerateListAccountDetail(accordion, "sectionbadprimarygroup" + root, "Objects having the primary group attribute changed ", data.ListBadPrimaryGroup); - } - if (data.ListPwdNotRequired != null && data.ListPwdNotRequired.Count > 0) - { - GenerateListAccountDetail(accordion, "sectionpwdnotrequired" + root, "Objects which can have an empty password ", data.ListPwdNotRequired); - } - if (data.ListDesEnabled != null && data.ListDesEnabled.Count > 0) - { - GenerateListAccountDetail(accordion, "sectiondesenabled" + root, "Objects which can use DES in kerberos authentication ", data.ListDesEnabled); - } - if (data.ListTrustedToAuthenticateForDelegation != null && data.ListTrustedToAuthenticateForDelegation.Count > 0) - { - GenerateListAccountDetail(accordion, "sectiontrusteddelegation" + root, "Objects trusted to authenticate for delegation ", data.ListTrustedToAuthenticateForDelegation); - } - if (data.ListTrustedToAuthenticateForDelegation != null && data.ListTrustedToAuthenticateForDelegation.Count > 0) - { - GenerateListAccountDetail(accordion, "sectionreversible" + root, "Objects having a reversible password ", data.ListReversibleEncryption); - } - if (data.ListDuplicate != null && data.ListDuplicate.Count > 0) - { - GenerateListAccountDetail(accordion, "sectionduplicate" + root, "Objects being duplicates ", data.ListDuplicate); - } - if (data.ListNoPreAuth != null && data.ListNoPreAuth.Count > 0) - { - GenerateListAccountDetail(accordion, "sectionnopreauth" + root, "Objects without kerberos preauthentication ", data.ListNoPreAuth); - } - }); - } - - void SectionList(string accordion, string section, int value, List list) - { - if (value > 0 && list != null && list.Count > 0) - { - Add(@""); - Add(value); - Add(@""); - } - else - { - Add(value); - } - } - - void GenerateListAccountDetail(string accordion, string id, string title, List list) - { - if (list == null) - { - return; - } - Add(@" -
- -
-
-
- - - - - - - - - - "); - int number = 0; - list.Sort((HealthcheckAccountDetailData a, HealthcheckAccountDetailData b) - => - { - return String.Compare(a.Name, b.Name); - } - ); - foreach (HealthcheckAccountDetailData detail in list) - { - Add(@" - - - - - "); - number++; - if (number >= MaxNumberUsersInHtmlReport) - { - Add(""); - break; - } - } - Add(@" - -
NameCreationLast logonDistinguished name
"); - Add(Encode(detail.Name)); - Add(@""); - Add((detail.CreationDate > DateTime.MinValue ? detail.CreationDate.ToString("u") : "Access Denied")); - Add(@""); - Add((detail.LastLogonDate > DateTime.MinValue ? detail.LastLogonDate.ToString("u") : "Never")); - Add(@""); - Add(Encode(detail.DistinguishedName)); - Add(@"
Output limited to "); - Add(MaxNumberUsersInHtmlReport); - Add(" items - add \"--no-enum-limit\" to remove that limit
-
-
-
-
"); - } - - private void GenerateDomainSIDHistoryList(HealthcheckAccountData data) - { - if (data.ListDomainSidHistory == null || data.ListDomainSidHistory.Count == 0) - return; - - GenerateSubSection("SID History"); - Add(@" -
-
- - - - - - - - "); - data.ListDomainSidHistory.Sort( - (HealthcheckSIDHistoryData x, HealthcheckSIDHistoryData y) => - { - return String.Compare(x.FriendlyName, y.FriendlyName); - } - ); - foreach (HealthcheckSIDHistoryData domainSidHistory in data.ListDomainSidHistory) - { - Add(""); - } - Add(@" -
SID History from domainFirst date seenLast date seenCount
"); - Add(Encode(domainSidHistory.FriendlyName)); - Add(""); - Add(domainSidHistory.FirstDate); - Add(""); - Add(domainSidHistory.LastDate); - Add(""); - Add(domainSidHistory.Count); - Add("
-
-
"); - } - - #endregion user info - #region computer info - private void GenerateComputerInformation() - { - Add(@" -
-
- - - - - - - - - - - - - - - - - - - - - -
Nb Computer AccountsNb EnabledNb DisabledNb ActiveNb InactiveNb SidHistoryNb Bad PrimaryGroupNb Trusted delegationNb Reversible password
"); - Add(HealthCheckData.ComputerAccountData.Number); - Add(@""); - Add(HealthCheckData.ComputerAccountData.NumberEnabled); - Add(@""); - Add(HealthCheckData.ComputerAccountData.NumberDisabled); - Add(@""); - Add(HealthCheckData.ComputerAccountData.NumberActive); - Add(@""); - SectionList("computersaccordion", "sectioninactivecomputer", HealthCheckData.ComputerAccountData.NumberInactive, HealthCheckData.ComputerAccountData.ListInactive); - Add(@""); - SectionList("computersaccordion", "sectionsidhistorycomputer", HealthCheckData.ComputerAccountData.NumberSidHistory, HealthCheckData.ComputerAccountData.ListSidHistory); - Add(@""); - SectionList("computersaccordion", "sectionbadprimarygroupcomputer", HealthCheckData.ComputerAccountData.NumberBadPrimaryGroup, HealthCheckData.ComputerAccountData.ListBadPrimaryGroup); - Add(@""); - SectionList("computersaccordion", "sectiontrusteddelegationcomputer", HealthCheckData.ComputerAccountData.NumberTrustedToAuthenticateForDelegation, HealthCheckData.ComputerAccountData.ListTrustedToAuthenticateForDelegation); - Add(@""); - SectionList("computersaccordion", "sectionreversiblencomputer", HealthCheckData.ComputerAccountData.NumberReversibleEncryption, HealthCheckData.ComputerAccountData.ListReversibleEncryption); - Add(@"
-
-
-"); - GenerateListAccount(HealthCheckData.ComputerAccountData, "computer", "computersaccordion"); - GenerateOperatingSystemList(); - GenerateDomainSIDHistoryList(HealthCheckData.ComputerAccountData); - GenerateDCInformation(); - } - - private void GenerateOperatingSystemList() - { - GenerateSubSection("Operating Systems"); - bool oldOS = version <= new Version(2, 5, 0, 0); - if (oldOS) - { - Add(@" -
-
- - - - - - "); - HealthCheckData.OperatingSystem.Sort( - (HealthcheckOSData x, HealthcheckOSData y) => - { - return OrderOS(x.OperatingSystem, y.OperatingSystem); - } - ); - { - foreach (HealthcheckOSData os in HealthCheckData.OperatingSystem) - { - Add(""); - } - } - Add(@" -
Operating SystemCount
"); - Add(Encode(os.OperatingSystem)); - Add(""); - Add(os.NumberOfOccurence); - Add("
-
-
"); - } - else - { - Add(@" -
-
- - - - - - - - - - - - - - "); - HealthCheckData.OperatingSystem.Sort( - (HealthcheckOSData x, HealthcheckOSData y) => - { - return OrderOS(x.OperatingSystem, y.OperatingSystem); - } - ); - { - foreach (HealthcheckOSData os in HealthCheckData.OperatingSystem) - { - Add(@" - - - - - - "); - } - } - Add(@" -
Operating SystemNb OSNb EnabledNb DisabledNb ActiveNb InactiveNb SidHistoryNb Bad PrimaryGroupNb Trusted delegationNb Reversible password
"); - Add(os.OperatingSystem); - Add(@""); - Add(os.data.Number); - Add(@""); - Add(os.data.NumberEnabled); - Add(@""); - Add(os.data.NumberDisabled); - Add(@""); - Add(os.data.NumberActive); - Add(@""); - Add(os.data.NumberInactive); - Add(@""); - Add(os.data.NumberSidHistory); - Add(@""); - Add(os.data.NumberBadPrimaryGroup); - Add(@""); - Add(os.data.NumberTrustedToAuthenticateForDelegation); - Add(@""); - Add(os.data.NumberReversibleEncryption); - Add(@"
-
-
"); - } - } - - private void GenerateDCInformation() - { - if (HealthCheckData.DomainControllers == null || HealthCheckData.DomainControllers.Count == 0) - return; - - GenerateSubSection("Domain controllers"); - Add(@" -
- -
-
-
- - - - - - - - - - - - - - "); - - int count = 0; - foreach (var dc in HealthCheckData.DomainControllers) - { - count++; - Add(@" - - - - - - - - -"); - } - Add( @" - -
Domain controllerOSCreation DateStartup TimeUptimeOwnerNull sessionsSMB v1
"); - Add(Encode(dc.DCName)); - Add(@""); - Add(Encode(dc.OperatingSystem)); - Add(@""); - Add((dc.CreationDate == DateTime.MinValue ? "Unknown" : dc.CreationDate.ToString("u"))); - Add(@""); - Add((dc.StartupTime == DateTime.MinValue ? (dc.LastComputerLogonDate.AddDays(60) < DateTime.Now ? "Inactive?" : "Unknown") : (dc.StartupTime.AddMonths(6) < DateTime.Now ? "" + dc.StartupTime.ToString("u") + "" : dc.StartupTime.ToString("u")))); - Add(@""); - Add((dc.StartupTime == DateTime.MinValue ? "" : (DateTime.Now.Subtract(dc.StartupTime)).Days + " days")); - Add(@""); - Add((String.IsNullOrEmpty(dc.OwnerName) ? dc.OwnerSID : dc.OwnerName)); - Add(@""); - Add((dc.HasNullSession ? "YES" : "NO")); - Add(@""); - Add((dc.SupportSMB1 ? "YES" : "NO")); - Add(@"
-
-
-
-
-"); - } - - - #endregion computer info - - #region admin groups - private void GenerateAdminGroupsInformation() - { - if (HealthCheckData.PrivilegedGroups != null) - { - Add(@" -
-
- - - - - - - - -"); - if (version > new Version(2, 5, 2)) - { - Add(@""); - } - Add(@" - - - - - -"); - HealthCheckData.PrivilegedGroups.Sort((HealthCheckGroupData a, HealthCheckGroupData b) - => - { - return String.Compare(a.GroupName, b.GroupName); - } - ); - foreach (HealthCheckGroupData group in HealthCheckData.PrivilegedGroups) - { - Add(@" - - - - - - - - - - - - "); - } - Add(@" - -
Group NameNb AdminsNb EnabledNb DisabledNb InactiveNb PWd never expireNb Smart Card requiredNb can be delegatedNb external users
"); - Add((group.Members != null && group.Members.Count > 0 ? @"" + Encode(group.GroupName) + "" : Encode(group.GroupName))); - Add(@""); - Add(group.NumberOfMember); - Add(@""); - Add(group.NumberOfMemberEnabled); - Add(@""); - Add(group.NumberOfMemberDisabled); - Add(@""); - Add(group.NumberOfMemberInactive); - Add(@""); - Add(group.NumberOfMemberPwdNeverExpires); - if (version > new Version(2, 5, 2)) - { - Add(@""); - Add(group.NumberOfSmartCardRequired); - } - Add(@""); - Add(group.NumberOfMemberCanBeDelegated); - Add(@""); - Add(group.NumberOfExternalMember); - Add(@"
-
-
-"); - foreach (HealthCheckGroupData group in HealthCheckData.PrivilegedGroups) - { - if (group.Members != null && group.Members.Count > 0) - { - GenerateModalAdminGroup(GenerateModalAdminGroupIdFromGroupName(group.GroupName), group.GroupName, - () => GenerateAdminGroupsDetail(group.Members)); - } - } - } - - if (HealthCheckData.AllPrivilegedMembers != null && HealthCheckData.AllPrivilegedMembers.Count > 0) - { - GenerateAccordion("admingroups", - () => - { - Add(@" -
- -
-
-
-"); - GenerateAdminGroupsDetail(HealthCheckData.AllPrivilegedMembers); - Add(@" -
-
-
-
-"); - }); - } - - if (HealthCheckData.Delegations != null && HealthCheckData.Delegations.Count > 0) - { - Add(@" -
- -
-
-
-"); - GenerateDelegationDetail(); - Add(@" -
-
-
-
-"); - } - } - private string GenerateModalAdminGroupIdFromGroupName(string groupname) - { - return "modal" + groupname.Replace(" ", "-").Replace("<",""); - } - private void GenerateModalAdminGroup(string id, string title, GenerateContentDelegate content) - { - Add(@" - -
-
- -
-
- - " + title + @" -
-
-
-"); - content(); - Add(@" -
-
-
- -
-
-
-
- -"); - } - - private void GenerateDelegationDetail() - { - Add(@" - - - - - - - -"); - HealthCheckData.Delegations.Sort(OrderDelegationData); - - foreach (HealthcheckDelegationData delegation in HealthCheckData.Delegations) - { - int dcPathPos = delegation.DistinguishedName.IndexOf(",DC="); - string path = delegation.DistinguishedName; - if (dcPathPos > 0) - path = delegation.DistinguishedName.Substring(0, dcPathPos); - Add(@" - - - - -"); - } - Add(@" -
DistinguishedNameAccountRight
"); - Add(Encode(path)); - Add(@""); - Add(Encode(delegation.Account)); - Add(@""); - Add(Encode(delegation.Right)); - Add(@"
-
"); - } - - private void GenerateAdminGroupsDetail(List members) - { - if (members != null) - { - Add(@" - - - - - - - -"); - if (version > new Version(2, 5, 2)) - { - Add(@""); - } - Add(@" - - - - -"); - members.Sort((HealthCheckGroupMemberData a,HealthCheckGroupMemberData b) - => - { - return String.Compare(a.Name, b.Name); - } - ); - foreach (HealthCheckGroupMemberData member in members) - { - if (member.IsExternal) - { - Add(@" - - - - - - - - - -"); - } - else - { - Add(@" - - - - - - - - - -"); - } - } - Add(@" -
SamAccountNameEnabledActivePwd never ExpiredLockedSmart Card requiredFlag Cannot be delegated presentDistinguished name
"); - Add(Encode(member.Name)); - Add(@"ExternalExternalExternalExternalExternal"); - if (version > new Version(2, 5, 2)) - { - Add(@"External"); - } - Add(Encode(member.DistinguishedName)); - Add(@"
"); - Add(Encode(member.Name)); - Add(@""); - Add((member.IsEnabled ? "" : "")); - Add(@""); - Add((member.IsActive ? "" : "")); - Add(@""); - Add((member.DoesPwdNeverExpires ? "YES" : "NO")); - Add(@""); - Add((member.IsLocked ? "YES" : "NO")); - Add(@""); - if (version > new Version(2, 5, 2)) - { - Add((member.SmartCardRequired ? "YES" : "NO")); - Add(@""); - } - Add((!member.CanBeDelegated ? "YES" : "NO")); - Add(@""); - Add(Encode(member.DistinguishedName)); - Add(@"
-"); - } - } - - // revert an OU string order to get a string orderable - // ex: OU=myOU,DC=DC => DC=DC,OU=myOU - private string GetDelegationSortKey(HealthcheckDelegationData a) - { - string[] apart = a.DistinguishedName.Split(','); - string[] apart1 = new string[apart.Length]; - for (int i = 0; i < apart.Length; i++) - { - apart1[i] = apart[apart.Length - 1 - i]; - } - return String.Join(",", apart1); - } - private int OrderDelegationData(HealthcheckDelegationData a, HealthcheckDelegationData b) - { - if (a.DistinguishedName == b.DistinguishedName) - return String.Compare(a.Account, b.Account); - return String.Compare(GetDelegationSortKey(a), GetDelegationSortKey(b)); - } - - #endregion admin groups - - #region trust - void GenerateTrustInformation() - { - List knowndomains = new List(); - GenerateSubSection("Discovered Domains"); - Add(@" -
-
- - - - - - - - - - - - -"); - foreach (HealthCheckTrustData trust in HealthCheckData.Trusts) - { - string sid = (string.IsNullOrEmpty(trust.SID) ? "[Unknown]" : trust.SID); - string netbios = (string.IsNullOrEmpty(trust.NetBiosName) ? "[Unknown]" : trust.NetBiosName); - string sidfiltering = TrustAnalyzer.GetSIDFiltering(trust); - if (sidfiltering == "Yes") - { - sidfiltering = "" + sidfiltering + ""; - } - else if (sidfiltering == "No") - { - sidfiltering = ""+ sidfiltering + ""; - } - Add(@" - - - - - - - - -"); - } - Add(@" - -
Trust PartnerTypeAttributDirectionSID Filtering activeCreationIs Active ?
"); - if (GetUrlCallback == null) - { - Add(@"Netbios: "); - Add(netbios); - Add(@""">"); - Add(Encode(trust.TrustPartner)); - Add(@""); - } - else - { - Add(GetUrlCallback(trust.Domain, trust.TrustPartner)); - } - Add(@""); - Add(TrustAnalyzer.GetTrustType(trust.TrustType)); - Add(@""); - Add(TrustAnalyzer.GetTrustAttribute(trust.TrustAttributes)); - Add(@""); - Add(TrustAnalyzer.GetTrustDirection(trust.TrustDirection)); - Add(@""); - Add(sidfiltering); - Add(@""); - Add(trust.CreationDate); - Add(@""); - Add((trust.IsActive ? trust.IsActive.ToString() : "False")); - Add(@"
-
-
-"); - - GenerateSubSection("Reachable Domains"); - - Add(@" -
-
- - - - - - - - - -"); - foreach (HealthCheckTrustData trust in HealthCheckData.Trusts) - { - if (trust.KnownDomains == null) - continue; - trust.KnownDomains.Sort((HealthCheckTrustDomainInfoData a, HealthCheckTrustDomainInfoData b) - => - { - return String.Compare(a.DnsName, b.DnsName); - } - ); - foreach (HealthCheckTrustDomainInfoData di in trust.KnownDomains) - { - Add(@" - - - - - -"); - } - } - if (HealthCheckData.ReachableDomains != null) - { - foreach (HealthCheckTrustDomainInfoData di in HealthCheckData.ReachableDomains) - { - Add(@" - - - - - -"); - } - } - - Add(@" - -
Reachable domainViaNetbiosCreation date
"); - if (GetUrlCallback == null) - { - Add(Encode(di.DnsName)); - } - else - { - Add(GetUrlCallback(di.Domain, di.DnsName)); - } - Add(@""); - if (GetUrlCallback == null) - { - Add(Encode(trust.TrustPartner)); - } - else - { - Add(GetUrlCallback(trust.Domain, trust.TrustPartner)); - } - Add(@""); - Add(Encode(di.NetbiosName)); - Add(@""); - Add(di.CreationDate); - Add(@"
"); - if (GetUrlCallback == null) - { - Add(Encode(di.DnsName)); - } - else - { - Add(GetUrlCallback(di.Domain, di.DnsName)); - } - Add(@"Unknown"); - Add(Encode(di.NetbiosName)); - Add(@"Unknown
-
-
-"); - } - #endregion trust - - #region anomaly - private void GenerateAnomalyDetail() - { - GenerateSubSection("Backup"); - Add(@" -
-

The program checks the last date of the AD backup. This date is computed using the replication metadata of the attribute dsaSignature (reference).

-

Last backup date: " + (HealthCheckData.LastADBackup == DateTime.MaxValue ? "Never" : (HealthCheckData.LastADBackup == DateTime.MinValue ? "Not checked (older version of PingCastle)" : HealthCheckData.LastADBackup.ToString("u"))) + @"

-
-"); - - GenerateSubSection("LAPS"); - Add(@" -
-

LAPS is used to have a unique local administrator password on all workstations / servers of the domain. -Then this password is changed at a fixed interval. The risk is when a local administrator hash is retrieved and used on other workstation in a pass-the-hash attack.

-

Mitigation: having a process when a new workstation is created or install LAPS and apply it through a GPO

-

LAPS installation date: " + (HealthCheckData.LAPSInstalled == DateTime.MaxValue ? "Never" : (HealthCheckData.LAPSInstalled == DateTime.MinValue ? "Not checked (older version of PingCastle)" : HealthCheckData.LAPSInstalled.ToString("u"))) + @"

-
-"); - GenerateSubSection("Windows Event Forwarding (WEF)"); - Add(@" -
-

Windows Event Forwarding is a native mechanism used to collect logs on all workstations / servers of the domain. -Microsoft recommends to Use Windows Event Forwarding to help with intrusion detection -Here is the list of servers configure for WEF found in GPO

-

Number of WEF servers configured: " + (HealthCheckData.GPOEventForwarding.Count) + @"

-
-"); - // wef - if (HealthCheckData.GPOEventForwarding.Count > 0) - { - Add(@" -
-
-
-
- -
-
-
- - - - - - - - -"); - // descending sort - HealthCheckData.GPOEventForwarding.Sort( - (GPOEventForwardingInfo a, GPOEventForwardingInfo b) - => - { - int comp = String.Compare(a.GPOName, b.GPOName); - if (comp == 0) - comp = (a.Order > b.Order ? 1 : (a.Order == b.Order ? 0 : -1)); - return comp; - } - ); - - foreach (var info in HealthCheckData.GPOEventForwarding) - { - Add(@" - - - - - -"); - } - Add(@" -
GPO NameOrderServer
"); - Add(Encode(info.GPOName)); - Add(@""); - Add(info.Order); - Add(@""); - Add(Encode(info.Server)); - Add(@"
-
-
-
-
-
-
-
-"); - } - - - // krbtgt - GenerateSubSection("krbtgt (Used for Golden ticket attacks)"); - Add(@" -
-

The password of the krbtgt account should be changed twice every 40 days using this script

-

You can use the version gathered using replication metadata from two reports to guess the frequency of the password change or if the two consecutive resets has been done

-

Kerberos password last changed: " + HealthCheckData.KrbtgtLastChangeDate.ToString("u") + @" -version: " + HealthCheckData.KrbtgtLastVersion + @" -

-
-"); - // adminSDHolder - GenerateSubSection("AdminSDHolder (detect temporary elevated accounts)"); - Add(@" -
-

This control detects accounts which are former 'unofficial' admins. -Indeed when an account belongs to a privileged group, the attribute adminaccount is set. If the attribute is set without being an official member, this is suspicious. To suppress this warning, the attribute admincount of these accounts should be removed after review.

-

Number of accounts to review: " + - (HealthCheckData.AdminSDHolderNotOKCount > 0 ? "" + HealthCheckData.AdminSDHolderNotOKCount + "" : "0") - + @"

-
-"); - if (HealthCheckData.AdminSDHolderNotOKCount > 0 && HealthCheckData.AdminSDHolderNotOK != null && HealthCheckData.AdminSDHolderNotOK.Count > 0) - { - GenerateAccordion("adminsdholder", () => GenerateListAccountDetail("adminsdholder", "adminsdholderpanel", "AdminSDHolder User List", HealthCheckData.AdminSDHolderNotOK)); - } - - if (HealthCheckData.DomainControllers != null) - { - string nullsession = null; - int countnullsession = 0; - foreach (var DC in HealthCheckData.DomainControllers) - { - if (DC.HasNullSession) - { - nullsession += @"" + DC.DCName + @""; - countnullsession++; - } - } - if (countnullsession > 0) - { - GenerateSubSection("NULL SESSION (anonymous access)"); - Add(@" -
-

Domain controllers vulnerable: " + countnullsession + @" -

-
-
-
-
- -
-
-
- - - - - - - " + nullsession + @" -
Domain Controller
-
-
-
-
-
-
-
-"); - } - - if (HealthCheckData.SmartCardNotOK != null && HealthCheckData.SmartCardNotOK.Count > 0) - { - // smart card - GenerateSubSection("Smart Card Password (Password change for smartcard users)"); - Add(@" -
-

Users smart card and password > 40 days: " + - (HealthCheckData.SmartCardNotOK == null ? 0 : HealthCheckData.SmartCardNotOK.Count) - + @"

-
-"); - GenerateAccordion("anomalysmartcard", () => GenerateListAccountDetail("anomalysmartcard", "smartcard", "Smart card Password >40 days List", HealthCheckData.SmartCardNotOK)); - } - - // logon script - GenerateSubSection("Logon scripts"); - Add(@" -
-

You can check here backdoors or typo error in the scriptPath attribute

-
-
- - - - - - - -"); - // descending sort - HealthCheckData.LoginScript.Sort( - (HealthcheckLoginScriptData a, HealthcheckLoginScriptData b) - => - { - return b.NumberOfOccurence.CompareTo(a.NumberOfOccurence); - } - ); - - int number = 0; - foreach (HealthcheckLoginScriptData script in HealthCheckData.LoginScript) - { - Add(@" - - - - -"); - number++; - if (number >= MaxNumberUsersInHtmlReport) - { - Add(""); - break; - } - } - Add(@" - -
Script NameCount
"); - Add(Encode(String.IsNullOrEmpty(script.LoginScript.Trim()) ? "" : script.LoginScript)); - Add(@""); - Add(script.NumberOfOccurence); - Add(@"
Output limited to "); - Add(MaxNumberUsersInHtmlReport); - Add(" items - add \"--no-enum-limit\" to remove that limit
-
-"); - // certificate - GenerateSubSection("Certificates"); - Add(@" -
-

This detects trusted certificate which can be used in man in the middle attacks or which can issue smart card logon certificates

-

Number of trusted certificates: " + HealthCheckData.TrustedCertificates.Count + @" -

-
-
-
-
- -
-
-
- - - - - - - - - - - - - - -"); - foreach (HealthcheckCertificateData data in HealthCheckData.TrustedCertificates) - { - X509Certificate2 cert = new X509Certificate2(data.Certificate); - bool SCLogonAllowed = false; - foreach (X509Extension ext in cert.Extensions) - { - if (ext.Oid.Value == "1.3.6.1.4.1.311.20.2.2") - { - SCLogonAllowed = true; - break; - } - } - int modulesize = 0; - RSA key = null; - try - { - key = cert.PublicKey.Key as RSA; - } - catch (Exception) - { - } - if (key != null) - { - RSAParameters rsaparams = key.ExportParameters(false); - modulesize = rsaparams.Modulus.Length * 8; - } - Add(@" - - - - - - - - - - - -"); - } - Add(@" -
SourceStoreSubjectIssuerNotBeforeNotAfterModule sizeSignature AlgSC Logon
"); - Add(Encode(data.Source)); - Add(@""); - Add(Encode(data.Store)); - Add(@""); - Add(Encode(cert.Subject)); - Add(@""); - Add(Encode(cert.Issuer)); - Add(@""); - Add(cert.NotBefore); - Add(@""); - Add(cert.NotAfter); - Add(@""); - Add(modulesize); - Add(@""); - Add(cert.SignatureAlgorithm.FriendlyName); - Add(@""); - Add(SCLogonAllowed); - Add(@"
-
-
-
-
-
-
-
- -"); - } - } - #endregion anomaly - - #region password policies - - private void GeneratePasswordPoliciesDetail() - { - GenerateSubSection("Password policies"); - Add(@" -

Note: PSO (Password Settings Objects) will be visible only if the user which collected the information has the permission to view it.
PSO shown in the report will be prefixed by "PSO:"

-
- - - - - - - - - - - - - - - -"); - if (HealthCheckData.GPPPasswordPolicy != null) - { - foreach (GPPSecurityPolicy policy in HealthCheckData.GPPPasswordPolicy) - { - Add(@" - - - - - - - - - - - - -"); - } - } - Add(@" - -
Policy NameComplexityMax Password AgeMin Password AgeMin Password LengthPassword HistoryReversible EncryptionLockout ThresholdLockout DurationReset account counter locker after
"); - Add(Encode(policy.GPOName)); - Add(@""); - Add(GetPSOStringValue(policy, "PasswordComplexity")); - Add(@""); - Add(GetPSOStringValue(policy, "MaximumPasswordAge")); - Add(@""); - Add(GetPSOStringValue(policy, "MinimumPasswordAge")); - Add(@""); - Add(GetPSOStringValue(policy, "MinimumPasswordLength")); - Add(@""); - Add(GetPSOStringValue(policy, "PasswordHistorySize")); - Add(@""); - Add(GetPSOStringValue(policy, "ClearTextPassword")); - Add(@""); - Add(GetPSOStringValue(policy, "LockoutBadCount")); - Add(@""); - Add(GetPSOStringValue(policy, "LockoutDuration")); - Add(@""); - Add(GetPSOStringValue(policy, "ResetLockoutCount")); - Add(@"
-
-"); - GenerateSubSection("Screensaver policies"); - Add(@" -
- - - - - - - - - - -"); - if (HealthCheckData.GPOScreenSaverPolicy != null) - { - foreach (GPPSecurityPolicy policy in HealthCheckData.GPOScreenSaverPolicy) - { - Add(@" - - - - - - - -"); - } - } - Add(@" - -
Policy NameScreensaver enforcedPassword requestStart after (seconds)Grace Period (seconds)
"); - Add(Encode(policy.GPOName)); - Add(@""); - Add(GetPSOStringValue(policy, "ScreenSaveActive")); - Add(@""); - Add(GetPSOStringValue(policy, "ScreenSaverIsSecure")); - Add(@""); - Add(GetPSOStringValue(policy, "ScreenSaveTimeOut")); - Add(@""); - Add(GetPSOStringValue(policy, "ScreenSaverGracePeriod")); - Add(@"
-
-"); - GenerateSubSection("LSA settings"); - Add(@" -
- - - - - - - "); - if (HealthCheckData.GPPPasswordPolicy != null) - { - foreach (GPPSecurityPolicy policy in HealthCheckData.GPOLsaPolicy) - { - foreach (GPPSecurityPolicyProperty property in policy.Properties) - { - Add(@" - - - - - -"); - } - } - } - Add(@" - -
Policy NameSettingValue
"); - Add(Encode(policy.GPOName)); - Add(@""); - Add(GetLinkForLsaSetting(property.Property)); - Add(@""); - Add(property.Value); - Add(@"
-
-"); - } - - #endregion password policies - - #region GPO - private void GenerateGPODetail() - { - GenerateSubSection("Obfuscated Passwords"); - Add(@" -
-

The password in GPO are obfuscated, not encrypted. Consider any passwords listed here as compromissed and change it immediatly.

-
-"); - if (HealthCheckData.GPPPassword != null && HealthCheckData.GPPPassword.Count > 0) - { - Add(@" -
- - - - - - - - - - - -"); - foreach (GPPPassword password in HealthCheckData.GPPPassword) - { - Add(@" - - - - - - - - - "); - } - Add(@" - -
GPO NamePassword originUserNamePasswordChangedOther
"); - Add(Encode(password.GPOName)); - Add(@""); - Add(Encode(password.Type)); - Add(@""); - Add(Encode(password.UserName)); - Add(@""); - Add(Encode(password.Password)); - Add(@""); - Add(password.Changed); - Add(@""); - Add(Encode(password.Other)); - Add(@"
-
-"); - } - - GenerateSubSection("Restricted Groups"); - Add(@" -
-

Giving local group membership in a GPO is a way to become administrator.
- The local admin of a domain controller can become domain administrator instantly.

-
-"); - if (HealthCheckData.GPOLocalMembership != null && HealthCheckData.GPOLocalMembership.Count > 0) - { - HealthCheckData.GPOLocalMembership.Sort((GPOMembership a, GPOMembership b) => - { - int sort = String.Compare(a.GPOName, b.GPOName); - if (sort == 0) - sort = String.Compare(a.User, b.User); - if (sort == 0) - sort = String.Compare(a.MemberOf, b.MemberOf); - return sort; - } - ); - Add(@" -
- - - - - - - - "); - - foreach (GPOMembership membership in HealthCheckData.GPOLocalMembership) - { - Add(@" - - - - - -"); - } - Add(@" - -
GPO NameUser or groupMember of
"); - Add(Encode(membership.GPOName)); - Add(@""); - Add(Encode(membership.User)); - Add(@""); - Add(Encode(membership.MemberOf)); - Add(@"
-
-"); - } - - GenerateSubSection("Privileges"); - Add(@" -
-

Giving privilegdes in a GPO is a way to become administrator without being part of a group.
- For example, SeTcbPriviledge give the right to act as SYSTEM, which has more privileges than the administrator account.

-
-"); - if (HealthCheckData.GPPRightAssignment != null && HealthCheckData.GPPRightAssignment.Count > 0) - { - Add(@" -
- - - - - - - - "); - - foreach (GPPRightAssignment right in HealthCheckData.GPPRightAssignment) - { - Add(@" - - - - - -"); - } - Add(@" - -
GPO NamePrivilegeMembers
"); - Add(Encode(right.GPOName)); - Add(@""); - Add(Encode(right.Privilege)); - Add(@""); - Add(Encode(right.User)); - Add(@"
-
-"); - } - GenerateSubSection("GPO Login script"); - Add(@" -
-

A GPO login script is a way to force the execution of data on behalf of users.

-
-"); - if (HealthCheckData.GPOLoginScript != null && HealthCheckData.GPOLoginScript.Count > 0) - { - Add(@" -
- - - - - - - - - - "); - - foreach (HealthcheckGPOLoginScriptData loginscript in HealthCheckData.GPOLoginScript) - { - Add(@" - - - - - - - -"); - } - Add(@" - -
GPO NameActionSourceCommand lineParameters
"); - Add(Encode(loginscript.GPOName)); - Add(@""); - Add(Encode(loginscript.Action)); - Add(@""); - Add(Encode(loginscript.Source)); - Add(@""); - Add(Encode(loginscript.CommandLine)); - Add(@""); - Add(Encode(loginscript.Parameters)); - Add(@"
-
-"); - } - } - #endregion GPO - } -} diff --git a/Report/IPingCastleReportUser.cs b/Report/IPingCastleReportUser.cs new file mode 100644 index 0000000..82dd399 --- /dev/null +++ b/Report/IPingCastleReportUser.cs @@ -0,0 +1,16 @@ +using PingCastle.Data; +using System; +using System.Collections.Generic; +using System.Text; + +namespace PingCastle.Report +{ + public delegate string GetUrlDelegate(DomainKey domainKey, string displayName); + + public interface IPingCastleReportUser where T : IPingCastleReport + { + string GenerateReportFile(T report, ADHealthCheckingLicense license, string filename); + string GenerateRawContent(T report); + void SetUrlDisplayDelegate(GetUrlDelegate uRLDelegate); + } +} diff --git a/Report/ReportBase.cs b/Report/ReportBase.cs new file mode 100644 index 0000000..2eb6593 --- /dev/null +++ b/Report/ReportBase.cs @@ -0,0 +1,715 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using PingCastle.template; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Text; +using PingCastle.Rules; +using PingCastle.Healthcheck; +using PingCastle.Data; +using System.Text.RegularExpressions; + +namespace PingCastle.Report +{ + public abstract class ReportBase + { + + protected StringBuilder sb = new StringBuilder(); + public delegate bool HasDomainAmbigousNameDelegate(DomainKey domainKey); + + protected GetUrlDelegate GetUrlCallback; + + public HasDomainAmbigousNameDelegate HasDomainAmbigousName { get; set; } + + public void SetUrlDisplayDelegate(GetUrlDelegate uRLDelegate) + { + GetUrlCallback = uRLDelegate; + } + + public string GenerateReportFile(string filename) + { + var reportSB = new StringBuilder(TemplateManager.LoadResponsiveTemplate()); + + Hook(reportSB); + + sb.Length = 0; + GenerateTitleInformation(); + reportSB = reportSB.Replace("<%=Title%>", sb.ToString()); + + sb.Length = 0; + GenerateBaseHeaderInformation(); + GenerateHeaderInformation(); + reportSB = reportSB.Replace("<%=Header%>", sb.ToString()); + + sb.Length = 0; + GenerateBodyInformation(); + reportSB = reportSB.Replace("<%=Body%>", sb.ToString()); + + sb.Length = 0; + GenerateBaseFooterInformation(); + GenerateFooterInformation(); + reportSB = reportSB.Replace("<%=Footer%>", sb.ToString()); + + var html = reportSB.ToString(); + File.WriteAllText(filename, html); + return html; + } + + private void GenerateBaseHeaderInformation() + { + Add(@""); + } + + private void GenerateBaseFooterInformation() + { + Add(@""); + } + + protected virtual void Hook(StringBuilder sbHtml) + { + + } + + protected void Add(int value) + { + sb.Append(value); + } + + protected void Add(bool value) + { + sb.Append(value); + } + + protected void Add(string text) + { + sb.Append(text); + } + + protected void AddEncoded(string text) + { + sb.Append(ReportHelper.Encode(text)); + } + + protected void Add(DateTime date) + { + sb.Append(date.ToString("u")); + } + + protected delegate void GenerateContentDelegate(); + + protected abstract void GenerateFooterInformation(); + + protected abstract void GenerateTitleInformation(); + + protected abstract void GenerateHeaderInformation(); + + protected abstract void GenerateBodyInformation(); + + public static string GetStyleSheetTheme() + { + return @" + + + +"; + } + + protected void GenerateNavigation(string title, string domain, DateTime generationDate) + { + Add(@" + +"); + } + + protected void GenerateAbout(string aboutString) + { + Add(@" + +
+
+ +
+
+

About

+ +
+
+
+
+"); + Add(aboutString); + Add(@" +
+
+
+
+ +
+
+ +
+
+"); + } + + protected void GenerateSection(string title, GenerateContentDelegate generateContent) + { + string id = "section" + title.Replace(" ", ""); + Add(@" + +
+
+
+
+
+ +
+"); + generateContent(); + Add(@" +
+
+
+
+
+
+ +"); + } + + protected void GenerateSubSection(string title, string section = null) + { + Add(@" + "); + if (!string.IsNullOrEmpty(section)) + { + Add(@" + +"); + } + Add(@" +
+

"); + AddEncoded(title); + Add(@"

+
+ "); + } + + protected void GenerateAccordion(string accordion, GenerateContentDelegate content) + { + Add(@" + +
+
+ "); + content(); + Add(@"
+ +"); + + } + protected void GenerateAccordionDetail(string id, string dataParent, string title, int? itemCount, bool RuleStyle, GenerateContentDelegate content) + { + Add(@" +
+ + + +"); + if (itemCount != null) + { + if (!RuleStyle) + { + Add(@"["); + Add((int)itemCount); + Add(@"]"); + } + else + { + Add(@"+ "); + Add((int)itemCount); + Add(@" Point(s)"); + } + } + Add(@" +
+ +
+
+ "); + content(); + Add(@" +
+
"); + } + + private static string GenerateId(string title) + { + return "section" + title.Replace(" ", ""); + } + + protected void GenerateTabHeader(string title, string selectedTab, bool defaultIfTabEmpty = false) + { + string id = GenerateId(title); + bool isActive = (String.IsNullOrEmpty(selectedTab) ? defaultIfTabEmpty : selectedTab == id); + Add(@"
  • "); + Add(title); + Add("
  • "); + } + + protected void GenerateSectionFluid(string title, GenerateContentDelegate generateContent, string selectedTab, bool defaultIfTabEmpty = false) + { + string id = GenerateId(title); + bool isActive = (String.IsNullOrEmpty(selectedTab) ? defaultIfTabEmpty : selectedTab == id); + Add(@" + +
    +
    +
    +
    +

    "); + Add(title); + Add(@"

    +
    +"); + generateContent(); + Add(@" +
    +
    +
    +
    + +"); + } + + protected int OrderOS(string os1, string os2) + { + int ios1 = OSToInt(os1); + int ios2 = OSToInt(os2); + if (ios1 > 0 && ios2 > 0) + { + if (ios1 > ios2) + return 1; + else if (ios1 < ios2) + return -1; + return 0; + } + else if (ios1 > 0) + return -1; + else if (ios2 > 0) + return 1; + return String.Compare(os1, os2); + } + + // this function is used to sort operating system based not on name but on its order + // the value returned doesn't have a meaning at all. It is used for a comparison & sort + protected int OSToInt(string os) + { + switch (os) + { + case "Windows XP": + return 1; + case "Windows Vista": + return 2; + case "Windows 7": + return 3; + case "Windows 8": + return 4; + case "Windows 10": + return 5; + case "Windows NT": + return 6; + case "Windows 2000": + return 7; + case "Windows 2003": + return 8; + case "Windows 2008": + return 9; + case "Windows 2012": + return 10; + case "Windows 2016": + return 11; + case "Windows 2019": + return 12; + case "Windows Embedded": + return 13; + case "OperatingSystem not set": + return 14; + } + return 0; + } + + protected string GetPSOStringValue(GPPSecurityPolicy policy, string propertyName) + { + foreach (var property in policy.Properties) + { + if (property.Property == propertyName) + { + if (property.Value == 0) + { + if (propertyName == "PasswordComplexity") + { + return "False"; + } + if (propertyName == "ClearTextPassword" + || propertyName == "ScreenSaveActive" || propertyName == "ScreenSaverIsSecure") + return "False"; + + } + if (property.Value == -1 && propertyName == "MaximumPasswordAge") + { + return "Never expires"; + } + if (property.Value == 1) + { + if (propertyName == "ClearTextPassword") + return "True"; + if (propertyName == "PasswordComplexity" + || propertyName == "ScreenSaveActive" || propertyName == "ScreenSaverIsSecure") + return "True"; + } + if (propertyName == "MinimumPasswordLength") + { + if (property.Value < 8) + { + return "" + property.Value.ToString() + ""; + } + } + if (propertyName == "MinimumPasswordAge") + { + if (property.Value == 0) + { + return "0 day"; + } + return property.Value.ToString() + " day(s)"; + } + if (propertyName == "MaximumPasswordAge") + { + return property.Value.ToString() + " day(s)"; + } + if (propertyName == "ResetLockoutCount" || propertyName == "LockoutDuration") + { + if (property.Value <= 0) + return "Infinite"; + return property.Value.ToString() + " minute(s)"; + } + return property.Value.ToString(); + } + } + return "Not Set"; + } + + protected string GetLinkForLsaSetting(string property) + { + switch (property.ToLowerInvariant()) + { + case "enableguestaccount": + return @"EnableGuestAccount"; + case "lsaanonymousnamelookup": + return @"LSAAnonymousNameLookup"; + case "everyoneincludesanonymous": + return @"EveryoneIncludesAnonymous"; + case "limitblankpassworduse": + return @"LimitBlankPasswordUse"; + case "forceguest": + return @"ForceGuest"; + case "lmcompatibilitylevel": + return @"LmCompatibilityLevel"; + case "NoLMHash": + return @"NoLMHash"; + case "restrictanonymous": + return @"RestrictAnonymous"; + case "restrictanonymoussam": + return @"RestrictAnonymousSam"; + + } + return property; + } + + protected void GenerateGauge(int percentage) + { + Add(@"050100"); + } + + protected void GenerateChartNeedle(int percentage) + { + double leftX, leftY, rightX, rightY, thetaRad, topX, topY; + NumberFormatInfo nfi = new NumberFormatInfo(); + nfi.NumberDecimalSeparator = "."; + thetaRad = percentage * Math.PI / 100; + topX = -144 * Math.Cos(thetaRad); + topY = -144 * Math.Sin(thetaRad); + leftX = -18 * Math.Cos(thetaRad - Math.PI / 2); + leftY = -18 * Math.Sin(thetaRad - Math.PI / 2); + rightX = -18 * Math.Cos(thetaRad + Math.PI / 2); + rightY = -18 * Math.Sin(thetaRad + Math.PI / 2); + Add("M "); + Add(leftX.ToString(nfi)); + Add(" "); + Add(leftY.ToString(nfi)); + Add(" L "); + Add(topX.ToString(nfi)); + Add(" "); + Add(topY.ToString(nfi)); + Add(" L "); + Add(rightX.ToString(nfi)); + Add(" "); + Add(rightY.ToString(nfi)); + } + + protected string PrintDomain(DomainKey key) + { + string label = key.DomainName; + if (String.IsNullOrEmpty(label)) + { + if (!String.IsNullOrEmpty(key.DomainNetBIOS)) + label = key.DomainNetBIOS; + else + label = key.DomainSID; + } + if (GetUrlCallback == null) + return label; + string htmlData = GetUrlCallback(key, label); + if (String.IsNullOrEmpty(htmlData)) + return label; + return htmlData; + } + + protected string NewLineToBR(string data) + { + if (String.IsNullOrEmpty(data)) + return data; + return data.Replace("\r\n", "
    \r\n"); + } + + } +} diff --git a/Report/ReportCompromiseGraph.cs b/Report/ReportCompromiseGraph.cs new file mode 100644 index 0000000..5932344 --- /dev/null +++ b/Report/ReportCompromiseGraph.cs @@ -0,0 +1,1416 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using PingCastle.Data; +using PingCastle.Healthcheck; +using PingCastle.Rules; +using PingCastle.template; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reflection; +using System.Text; + +namespace PingCastle.Report +{ + public class ReportCompromiseGraph : ReportRiskControls, IPingCastleReportUser + { + private CompromiseGraphData Report; + private ADHealthCheckingLicense _license; + private Version version; + + public string GenerateReportFile(CompromiseGraphData report, ADHealthCheckingLicense license, string filename) + { + Report = report; + _license = license; + version = new Version(Report.EngineVersion.Split(' ')[0]); + return GenerateReportFile(filename); + } + + public string GenerateRawContent(CompromiseGraphData report) + { + Report = report; + _license = null; + version = new Version(Report.EngineVersion.Split(' ')[0]); + sb.Length = 0; + GenerateContent(); + return sb.ToString(); + } + + protected override void Hook(StringBuilder sbHtml) + { + // full screen graphs + sbHtml.Replace("", ""); + sbHtml.Replace("", ""); + } + + protected override void GenerateTitleInformation() + { + sb.Append("PingCastle Compromission Graphs - "); + sb.Append(DateTime.Now.ToString("yyyy-MM-dd")); + } + + protected override void GenerateHeaderInformation() + { + Add(@""); + Add(ReportBase.GetStyleSheetTheme()); + + Add(GetRiskControlStyleSheet()); + Add(GetStyleSheet()); + Add(@""); + } + + private string GetStyleSheet() + { + return @""; + } + + protected override void GenerateBodyInformation() + { + GenerateNavigation("Active Directory Compromission Graph", Report.DomainFQDN, Report.GenerationDate); + GenerateAbout(@" +

    This page has been inspired from the tools Active Directory Control Paths, OVALI and BloodHound.

    +

    The goal is to understand if, by doing some actions, a user account can gain more privileges than expected. For example, if a helpdesk user can reset a password account which is the owner of the login script of a domain admin, this user can become domain administrator.

    +

    Users, groups and other objects are connected through arrows which explain these links. The more objects there are, the more care should be used to check the highlighted path.

    +

    The paths made by PingCastle have known limitations compared to other tools to produce its quick analysis:

    +
      +
    • PingCastle does not check for local server ACL like bloodhound does (file server, etc)
    • +
    • PingCastle does only perform its analysis on a single path direction. The report to understand what a simple user can do is not present.
    • +
    +

    This is a compromise between speed and accuracy.

    + +

    Generated by Ping Castle all rights reserved

    +

    Open source components:

    + +"); + Add(@" +
    + +

    " + Report.DomainFQDN + @" - Active Directory Compromission Graph

    +

    Date: " + Report.GenerationDate.ToString("yyyy-MM-dd") + @" - Engine version: " + Report.EngineVersion + @"

    +
    +
    +
    +
    +This report has been generated with the "); + Add(String.IsNullOrEmpty(_license.Edition) ? "Basic" : _license.Edition); + Add(@" Edition of PingCastle."); + if (String.IsNullOrEmpty(_license.Edition)) + { + Add(@" +
    Being part of a commercial package is forbidden (selling the information contained in the report).
    +If you are an auditor, you MUST purchase an Auditor license to share the development effort. +"); + } + Add(@"
    +
    + +"); + GenerateContent(); + } + + void GenerateContent() + { + GenerateSection("Active Directory Indicators", () => GenerateIndicators(Report, Report.AllRiskRules)); + GenerateSectionStaleObjects(); + GenerateSectionPrivilegedAccounts(); + GenerateSectionTrusts(); + GenerateSectionAnomalies(); + GenerateSectionDetailledAnalysis(); + } + + protected void GenerateObjectivesCard(RiskRuleCategory category) + { + RiskModelObjective previousObj = RiskModelObjective.None; + foreach (var rule in Report.RiskRules) + { + if (rule.Category != category) + continue; + if (previousObj != rule.Objective) + { + if (previousObj != RiskModelObjective.None) + { + Add(@" +
    +
    "); + } + else + { + Add(@"
    "); + } + Add(@" +
    +
    +

    "); + AddEncoded(ReportHelper.GetEnumDescription(rule.Objective)); + Add(@"

    +
    +
      + "); + } + Add(@"
    • = 75) + { + Add("card-risk-critical"); + } + else if (rule.Points >= 50) + { + Add("card-risk-high"); + } + else if (rule.Points >= 25) + { + Add("card-risk-medium"); + } + else + { + Add("card-risk-low"); + } + } + Add(@""">"); + if (rule.Achieved) + Add(@"✓ "); + else + Add(@"✗ "); + AddEncoded(rule.Rationale); + Add(@"Risk: "); + Add(rule.Points); + Add(@"
    • "); + previousObj = rule.Objective; + } + if (previousObj != RiskModelObjective.None) + { + Add(@"
    +
    +
    "); + Add(@" +
    +"); + } + } + + private void GenerateSectionStaleObjects() + { + GenerateSection("Stale Objects", () => + { + GenerateSubIndicator("Stale Objects", Report.GlobalScore, Report.StaleObjectsScore, "It is about operations related to user or computer objects"); + GenerateSubSection("Objectives"); + GenerateObjectivesCard(RiskRuleCategory.StaleObjects); + }); + } + + private void GenerateSectionPrivilegedAccounts() + { + GenerateSection("Privileged Accounts", () => + { + GenerateSubIndicator("Privileged Accounts", Report.GlobalScore, Report.PrivilegiedGroupScore, "It is about administrators of the Active Directory"); + GenerateSubSection("Objectives"); + GenerateObjectivesCard(RiskRuleCategory.PrivilegedAccounts); + }); + } + + private void GenerateSectionTrusts() + { + GenerateSection("Trusts", () => + { + GenerateSubIndicator("Trusts", Report.GlobalScore, Report.TrustScore, "It is about operations related to user or computer objects"); + GenerateSubSection("Objectives"); + GenerateObjectivesCard(RiskRuleCategory.Trusts); + GenerateSubSection("Foreign domains involved"); + if (Report.Dependancies.Count == 0) + { + Add(@"

    No operative link with other domains has been found.

    "); + return; + } + Add(@"

    The following table lists all the foreign domains whose compromission can impact this domain. The impact is listed by typology of objects.

    "); + Add(@"
    +
    + + + + + + +"); + int numTypology = 0; + foreach (var typology in (CompromiseGraphDataTypology[])Enum.GetValues(typeof(CompromiseGraphDataTypology))) + { + Add(@""); + numTypology++; + } + Add(@" + +"); + Add(@""); + for (int i = 0; i < numTypology; i++) + { + Add(@""); + } + Add(@""); + Add(@" + +"); + foreach (var header in Report.Dependancies) + { + Add(@""); + foreach (var typology in (CompromiseGraphDataTypology[])Enum.GetValues(typeof(CompromiseGraphDataTypology))) + { + bool found = false; + foreach (var item in header.Details) + { + if (item.Typology != typology) + continue; + found = true; + Add(@""); + break; + } + if (!found) + { + Add(@""); + } + } + Add(@""); + } + Add(@" +
    FQDNNetBIOSSID"); + AddEncoded(ReportHelper.GetEnumDescription(typology)); + Add(@"
    Group ?Resolved ?Unresolved ?
    "); + AddEncoded(header.FQDN); + Add(@""); + AddEncoded(header.Netbios); + Add(@""); + AddEncoded(header.Sid); + Add(@""); + Add(item.NumberOfGroupImpacted); + Add(@""); + Add(item.NumberOfResolvedItems); + Add(@""); + Add(item.NumberOfUnresolvedItems); + Add(@"
    +
    +
    +"); + }); + } + + private void GenerateSectionAnomalies() + { + GenerateSection("Anomalies analysis", () => + { + GenerateSubIndicator("Anomalies", Report.GlobalScore, Report.AnomalyScore, "It is about specific security control points"); + GenerateSubSection("Objectives"); + GenerateObjectivesCard(RiskRuleCategory.Anomalies); + GenerateSubSection("Indirect links"); + Add(@" +
    +
    + + +"); + Add(@" + + + + +"); + Add(@""); + Add(@" + + +"); + foreach (var objectRisk in (CompromiseGraphDataObjectRisk[])Enum.GetValues(typeof(CompromiseGraphDataObjectRisk))) + { + Add(@""); + bool found = false; + foreach (var analysis in Report.AnomalyAnalysis) + { + if (analysis.ObjectRisk != objectRisk) + continue; + found = true; + Add(@""); + break; + } + if (!found) + { + Add(@""); + } + Add(@" +"); + } + Add(@" + +
    Priority to remediate ?Critical Object Found ?Number of objects with Indirect ?Max number of indirect numbers ?Max ratio ?
    "); + AddEncoded(ReportHelper.GetEnumDescription(objectRisk)); + Add(@""); + Add((analysis.CriticalObjectFound ? "YES" : "NO")); + Add(@""); + Add(analysis.NumberOfObjectsWithIndirect); + Add(@""); + Add(analysis.MaximumIndirectNumber); + Add(@""); + Add(analysis.MaximumDirectIndirectRatio); + Add(@"
    +
    +
    +"); + }); + } + + private void GenerateSectionDetailledAnalysis() + { + GenerateSection("Detailled analysis", () => + { + foreach (var typology in (CompromiseGraphDataTypology[])Enum.GetValues(typeof(CompromiseGraphDataTypology))) + { + GenerateSubSection(ReportHelper.GetEnumDescription(typology)); + Add(@"
    +"); + DisplayGroupHeader(); + for (int i = 0; i < Report.Data.Count; i++) + { + var data = Report.Data[i]; + if (data.Typology != typology) + continue; + GenerateSummary(i, data); + } + Add(@" + +
    + +"); + } + Add(@" + +"); + for (int i = 0; i < Report.Data.Count; i++) + { + GenerateModalGraph(i); + GenerateUserModalMember(i); + GenerateModalIndirectMember(i); + GenerateModalDependancy(i); + GenerateModalRules(i); + GenerateModalComputerMember(i); + GenerateModalDeletedObjects(i); + } + }); + } + + private void GenerateModalRules(int i) + { + Add(@" + +
    +
    +
    +
    +

    "); + AddEncoded(Report.Data[i].Description); + Add(@"

    +
    +
    +"); + GenerateAccordion("rules" + i, () => + { + foreach (var rule in Report.RiskRules) + { + foreach (var impactedAsset in rule.ImpactedAssets) + { + if (impactedAsset.AssetName == Report.Data[i].Name) + { + GenerateIndicatorPanelDetail(rule, i, impactedAsset); + break; + } + } + } + }); + Add(@" +
    +
    + +
    +
    +
    +
    +"); + } + + private void GenerateModalDependancy(int i) + { + Add(@" + +
    +
    +
    +
    +

    "); + AddEncoded(Report.Data[i].Description); + Add(@"

    +
    +
    +"); + foreach (var dependancy in Report.Data[i].Dependancies) + { + Add(@"

    "); + if (!String.IsNullOrEmpty(dependancy.FQDN)) + { + AddEncoded(dependancy.FQDN); + } + else + { + Add("Unknown Domain"); + } + Add(@"

    "); + Add(@"
    +
    NetBios
    +
    "); + AddEncoded(dependancy.Netbios); + Add(@"
    +
    SID
    +
    "); + AddEncoded(dependancy.Sid); + Add(@"
    +
    "); + if (dependancy.NumberOfResolvedItems > 0) + { + Add(@"
    Resolved accounts ("); + Add(dependancy.NumberOfResolvedItems); + Add(@")
    "); + foreach (var account in dependancy.Items) + { + if (account.Sid != account.Name) + { + AddEncoded(account.Name); + Add(" ("); + AddEncoded(account.Sid); + Add(")
    "); + } + } + } + if (dependancy.NumberOfUnresolvedItems > 0) + { + Add(@"
    Unresolved accounts ("); + Add(dependancy.NumberOfUnresolvedItems); + Add(@")
    "); + foreach (var account in dependancy.Items) + { + if (account.Sid == account.Name) + { + AddEncoded(account.Sid); + Add("
    "); + } + } + } + } + Add(@" +
    +
    + +
    +
    +
    +
    "); + } + + private void GenerateModalIndirectMember(int i) + { + Add(@" + +
    +
    +
    +
    +

    "); + AddEncoded(Report.Data[i].Description); + Add(@"

    +
    +
    +

    Indirect Members

    +
    + + + + + + + + + + +"); + foreach (var member in Report.Data[i].IndirectMembers) + { + DisplayIndirectMember(member); + } + Add(@" + +
    NameSecurity IdentifierDistanceLast authorized objectPath
    +
    +
    +
    + +
    +
    +
    +
    +"); + } + + private void GenerateModalDeletedObjects(int i) + { + Add(@" + +
    +
    +
    +
    +

    "); + AddEncoded(Report.Data[i].Description); + Add(@"

    +
    +
    +

    Deleted objects

    +
    + + + + + + +"); + foreach (var member in Report.Data[i].DeletedObjects) + { + Add(@" + + +"); + } + Add(@" + +
    Security Identifier
    "); + AddEncoded(member.Sid); + Add(@"
    +
    +
    +
    + +
    +
    +
    +
    +"); + } + + private void DisplayIndirectMember(SingleCompromiseGraphIndirectMemberData member) + { + Add(@" + "); + AddEncoded(member.Name); + Add(@" + "); + AddEncoded(member.Sid); + Add(@" + "); + Add(member.Distance); + Add(@" + "); + AddEncoded(member.AuthorizedObject); + Add(@" + "); + AddEncoded(member.Path); + Add(@" +"); + } + + private void GenerateUserModalMember(int i) + { + Add(@" + +
    +
    +
    +
    +

    "); + AddEncoded(Report.Data[i].Description); + Add(@"

    +
    +
    +

    Direct User Members

    +
    + + + + + + + + + + + + + + +"); + foreach (var member in Report.Data[i].DirectUserMembers) + { + DisplayUserMember(member); + } + Add(@" + +
    SamAccountNameEnabledActivePwd never ExpiredLockedSmart Card requiredService accountFlag Cannot be delegated presentDistinguished name
    +
    +
    +
    + +
    +
    +
    +
    +"); + } + + private void GenerateModalComputerMember(int i) + { + Add(@" + +
    +
    +
    +
    +

    "); + AddEncoded(Report.Data[i].Description); + Add(@"

    +
    +
    +

    Direct Computer Members

    +
    + + + + + + + + + + + +"); + foreach (var member in Report.Data[i].DirectComputerMembers) + { + DisplayComputerMember(member); + } + Add(@" + +
    SamAccountNameEnabledActiveLockedFlag Cannot be delegated presentDistinguished name
    +
    +
    +
    + +
    +
    +
    +
    +"); + } + + private void GenerateModalGraph(int i) + { + Add(@" + +
    +
    +
    +
    +

    "); + AddEncoded(Report.Data[i].Description); + Add(@"

    +
    +
    +
    +
    + 0% +
    +
    +
    + +
    + Legend:
    + u user
    + w external user or group
    + m computer
    + g group
    + o OU
    + x GPO
    + ? Other +
    +
    +
    + +
    +
    +
    +
    "); + } + + void GenerateIndicatorPanelDetail(CompromiseGraphRiskRule rule, int index, CompromiseGraphRiskRuleDetail detail) + { + string safeRuleId = rule.RiskId.Replace("$", "dollar"); + GenerateAccordionDetail("rulesdetail" + index + safeRuleId, "rules" + index, detail.Rationale, rule.Points, false, + () => + { + + var hcrule = RuleSet.GetRuleFromID(rule.RiskId); + if (hcrule == null) + { + } + else + { + Add("

    "); + AddEncoded(hcrule.Title); + Add("

    \r\nDescription:

    "); + Add(NewLineToBR(hcrule.Description)); + Add("

    \r\nTechnical explanation:

    "); + Add(NewLineToBR(hcrule.TechnicalExplanation)); + Add("

    \r\nAdvised solution:

    "); + Add(NewLineToBR(hcrule.Solution)); + Add("

    \r\nPoints:

    "); + Add(NewLineToBR(hcrule.GetComputationModelString())); + Add("

    \r\n"); + if (!String.IsNullOrEmpty(hcrule.Documentation)) + { + Add("Documentation:

    "); + Add(hcrule.Documentation); + Add("

    "); + } + } + if (detail.Details != null && detail.Details.Count > 0) + { + Add("Details:

    "); + Add(String.Join("
    \r\n", detail.Details.ToArray())); + Add("

    "); + } + }); + } + + private void DisplayUserMember(SingleCompromiseGraphUserMemberData member) + { + Add(@" + "); + AddEncoded(member.Name); + Add(@" + "); + Add((member.IsEnabled ? "" : "")); + Add(@" + "); + Add((member.IsActive ? "" : "")); + Add(@" + "); + Add((member.DoesPwdNeverExpires ? "YES" : "NO")); + Add(@" + "); + Add((member.IsLocked ? "YES" : "NO")); + Add(@" + "); + Add((member.SmartCardRequired ? "YES" : "NO")); + Add(@" + "); + Add((member.IsService ? "YES" : "NO")); + Add(@" + "); + Add((!member.CanBeDelegated ? "YES" : "NO")); + Add(@" + "); + AddEncoded(member.DistinguishedName); + Add(@" + +"); + } + + private void DisplayComputerMember(SingleCompromiseGraphComputerMemberData member) + { + Add(@" + "); + AddEncoded(member.Name); + Add(@" + "); + Add((member.IsEnabled ? "" : "")); + Add(@" + "); + Add((member.IsActive ? "" : "")); + Add(@" + "); + Add((member.IsLocked ? "YES" : "NO")); + Add(@" + "); + Add((!member.CanBeDelegated ? "YES" : "NO")); + Add(@" + "); + AddEncoded(member.DistinguishedName); + Add(@" + +"); + } + + private void DisplayGroupHeader() + { + Add(@" +
    + + + + + + + + + + + + + + +"); + } + + private void GenerateSummary(int index, SingleCompromiseGraphData data) + { + Add(@""); + } + + protected override void GenerateFooterInformation() + { + Add(@" + +"); + } + + + string BuildJasonFromSingleCompromiseGraph(SingleCompromiseGraphData data) + { + StringBuilder output = new StringBuilder(); + Dictionary idconversiontable = new Dictionary(); + output.Append("{"); + // START OF NODES + + output.Append(" \"nodes\": ["); + // it is important to put the root node as the first node for correct display + for (int i = 0; i < data.Nodes.Count; i++) + { + var node = data.Nodes[i]; + if (i != 0) + output.Append(" },"); + output.Append(" {"); + output.Append(" \"id\": " + node.Id + ","); + output.Append(" \"name\": \"" + ReportHelper.EscapeJsonString(node.Name) + "\","); + output.Append(" \"type\": \"" + node.Type + "\","); + output.Append(" \"shortName\": \"" + ReportHelper.EscapeJsonString(node.ShortName) + "\","); + if (node.Suspicious) + { + output.Append(" \"suspicious\": 1,"); + } + if (node.Critical) + { + output.Append(" \"critical\": 1,"); + } + if (node.Distance == 0) + output.Append(" \"dist\": null"); + else + output.Append(" \"dist\": \"" + node.Distance + "\""); + } + output.Append(" }"); + output.Append(" ],"); + // END OF NODES + + // START LINKS + output.Append(" \"links\": ["); + // avoid a final "," + for (int i = 0; i < data.Links.Count; i++) + { + var relation = data.Links[i]; + if (i != 0) + output.Append(" },"); + + output.Append(" {"); + output.Append(" \"source\": " + relation.Source + ","); + output.Append(" \"target\": " + relation.Target + ","); + output.Append(" \"rels\": ["); + for (int j = 0; j < relation.Hints.Count; j++) + { + output.Append(" \"" + data.Links[i].Hints[j] + "\"" + (j == relation.Hints.Count - 1 ? String.Empty : ",")); + } + + output.Append(" ]"); + } + if (data.Links.Count > 0) + { + output.Append(" }"); + } + output.Append(" ]"); + // END OF LINKS + output.Append("}"); + return output.ToString(); + } + + } +} diff --git a/Report/ReportCompromiseGraphConsolidation.cs b/Report/ReportCompromiseGraphConsolidation.cs new file mode 100644 index 0000000..3fd0371 --- /dev/null +++ b/Report/ReportCompromiseGraphConsolidation.cs @@ -0,0 +1,288 @@ +using PingCastle.Data; +using PingCastle.template; +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reflection; +using System.Text; + +namespace PingCastle.Report +{ + public class ReportCompromiseGraphConsolidation : ReportBase + { + private PingCastleReportCollection Report; + + public string GenerateReportFile(PingCastleReportCollection report, ADHealthCheckingLicense license, string filename) + { + Report = report; + return GenerateReportFile(filename); + } + + public string GenerateRawContent(PingCastleReportCollection report, string selectedTab = null) + { + Report = report; + sb.Length = 0; + GenerateContent(selectedTab); + return sb.ToString(); + } + + protected override void GenerateFooterInformation() + { + Add(@""); + Add(@" + +"); + } + + protected override void GenerateTitleInformation() + { + Add("PingCastle Consolidation report - "); + Add(DateTime.Now.ToString("yyyy-MM-dd")); + } + + protected override void GenerateHeaderInformation() + { + Add(@""); + Add(GetStyleSheetTheme()); + //Add(GetStyleSheet()); + } + + protected override void Hook(StringBuilder sbHtml) + { + sbHtml.Replace("", @""); + } + + protected override void GenerateBodyInformation() + { + Version version = Assembly.GetExecutingAssembly().GetName().Version; + string versionString = version.ToString(4); +#if DEBUG + versionString += " Beta"; +#endif + GenerateNavigation("Consolidation", null, DateTime.Now); + GenerateAbout(@"

    Generated by Ping Castle all rights reserved

    +

    Open source components:

    +"); + Add(@" +
    + +

    Consolidation

    +

    Date: " + DateTime.Now.ToString("yyyy-MM-dd") + @" - Engine version: " + versionString + @"

    +
    +"); + GenerateContent(); + Add(@" +
    +"); + } + + void GenerateContent(string selectedTab = null) + { + Add(@" +
    +
    +
      "); + GenerateTabHeader("Anomalies", selectedTab, true); + GenerateTabHeader("Trusts", selectedTab); + Add(@" +
    +
    +
    +
    +
    +
    "); + GenerateSectionFluid("Anomalies", GenerateAnomalies, selectedTab, true); + GenerateSectionFluid("Trusts", GenerateTrusts, selectedTab); + Add(@" +
    +
    +
    "); + } + + private void GenerateAnomalies() + { + List knowndomains = new List(); + GenerateSubSection("Link with other domains"); + Add(@" +
    +
    +
    Group or user account ?Priority ?Number of users member of the group ?Number of computer member of the group ?Number of object having indirect control ?Number of unresolved members (removed?) ?Link with other domainsRules triggeredDetail
    "); + AddEncoded(data.Description); + Add(@""); + AddEncoded(ReportHelper.GetEnumDescription(data.ObjectRisk)); + Add(@""); + if (data.DirectUserMembers.Count > 0) + { + Add(data.DirectUserMembers.Count); + Add(@" "); + Add("(Details)"); + Add(@""); + } + else + { + Add(0); + } + Add(@""); + if (data.DirectComputerMembers.Count > 0) + { + Add(data.DirectComputerMembers.Count); + Add(@" "); + Add("(Details)"); + Add(@""); + } + else + { + Add(0); + } + Add(@""); + if (data.IndirectMembers.Count > 0) + { + Add(data.IndirectMembers.Count); + Add(@" "); + Add("(Details)"); + Add(@""); + } + else + { + Add(0); + } + Add(@""); + if (data.DeletedObjects.Count != 0) + { + Add(data.DeletedObjects.Count); + Add(@" "); + Add("(Details)"); + Add(@""); + } + else + { + Add(0); + } + Add(@""); + if (data.Dependancies.Count != 0) + { + for (int i = 0; i < data.Dependancies.Count; i++) + { + var d = data.Dependancies[i]; + if (i > 0) + Add("
    "); + Add(@""); + if (!String.IsNullOrEmpty(d.Netbios)) + { + AddEncoded(d.Netbios); + } + else + { + Add("Unknown Domain "); + Add(i); + } + Add("["); + Add(d.NumberOfResolvedItems); + Add("+"); + Add(d.NumberOfUnresolvedItems); + Add("]"); + } + } + else + { + Add(@"None"); + } + Add(@"
    "); + int ruleCount = 0; + foreach(var rule in Report.RiskRules) + { + foreach (var impactedAsset in rule.ImpactedAssets) + { + if (impactedAsset.AssetName == data.Name) + { + ruleCount++; + break; + } + } + } + if (ruleCount == 0) + { + Add(@"0 rule triggered"); + } + else + { + Add(@""); + Add(ruleCount); + Add(" rule(s) triggered"); + } + Add(@"Analysis"); + Add(@"
    + + +"); + int numRisk = 0; + foreach (var objectRisk in (CompromiseGraphDataObjectRisk[])Enum.GetValues(typeof(CompromiseGraphDataObjectRisk))) + { + Add(@""); + numRisk++; + } + Add(@" +"); + Add(@""); + for (int i = 0; i < numRisk; i++) + { + Add(@" + + +"); + } + Add(@""); + Add(@" + + +"); + foreach (var data in Report) + { + Add(@" + + +"); + foreach (var objectRisk in (CompromiseGraphDataObjectRisk[])Enum.GetValues(typeof(CompromiseGraphDataObjectRisk))) + { + bool found = false; + foreach (var analysis in data.AnomalyAnalysis) + { + if (analysis.ObjectRisk != objectRisk) + continue; + found = true; + Add(@""); + break; + } + if (!found) + { + Add(@""); + } + } + Add(@" +"); + } + Add(@" + +
    Domain"); + AddEncoded(ReportHelper.GetEnumDescription(objectRisk)); + Add(@"
    Critical Object Found ?Number of objects with Indirect ?Max number of indirect numbers ?Max ratio ?
    " + PrintDomain(data.Domain) + @""); + Add((analysis.CriticalObjectFound ? "YES" : "NO")); + Add(@""); + Add(analysis.NumberOfObjectsWithIndirect); + Add(@""); + Add(analysis.MaximumIndirectNumber); + Add(@""); + Add(analysis.MaximumDirectIndirectRatio); + Add(@"
    +
    + +"); + } + + private void GenerateTrusts() + { + List knowndomains = new List(); + GenerateSubSection("Link with other domains"); + Add(@" +
    +
    + + + + +"); + int numTypology = 0; + foreach (var typology in (CompromiseGraphDataTypology[])Enum.GetValues(typeof(CompromiseGraphDataTypology))) + { + Add(@""); + numTypology++; + } + Add(@" +"); + Add(@""); + for (int i = 0; i < numTypology; i++) + { + Add(@""); + } + Add(@""); + Add(@" + + +"); + foreach (var data in Report) + { + + if (!knowndomains.Contains(data.DomainFQDN)) + knowndomains.Add(data.DomainFQDN); + + foreach (var dependancy in data.Dependancies) + { + if (!knowndomains.Contains(dependancy.FQDN)) + knowndomains.Add(dependancy.FQDN); + Add(@" + + + +"); + foreach (var typology in (CompromiseGraphDataTypology[])Enum.GetValues(typeof(CompromiseGraphDataTypology))) + { + bool found = false; + foreach (var item in dependancy.Details) + { + if (item.Typology != typology) + continue; + found = true; + Add(@""); + break; + } + if (!found) + { + Add(@""); + } + } + Add(@" +"); + } + } + Add(@" + +
    DomainRemote Domain"); + AddEncoded(ReportHelper.GetEnumDescription(typology)); + Add(@"
    Group ?Resolved ?Unresolved ?
    " + PrintDomain(data.Domain) + @"" + PrintDomain(dependancy.Domain) + @""); + Add(item.NumberOfGroupImpacted); + Add(@""); + Add(item.NumberOfResolvedItems); + Add(@""); + Add(item.NumberOfUnresolvedItems); + Add(@"
    +
    +
    +"); + } + } +} diff --git a/Report/ReportHealthCheckConsolidation.cs b/Report/ReportHealthCheckConsolidation.cs new file mode 100644 index 0000000..b528a7a --- /dev/null +++ b/Report/ReportHealthCheckConsolidation.cs @@ -0,0 +1,1360 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; +using PingCastle.Data; +using PingCastle.Healthcheck; +using PingCastle.Rules; +using PingCastle.template; + +namespace PingCastle.Report +{ + public class ReportHealthCheckConsolidation : ReportRiskControls + { + private PingCastleReportCollection Report; + + public string GenerateReportFile(PingCastleReportCollection report, ADHealthCheckingLicense license, string filename) + { + Report = report; + return GenerateReportFile(filename); + } + + public string GenerateRawContent(PingCastleReportCollection report, string selectedTab = null) + { + Report = report; + sb.Length = 0; + GenerateContent(selectedTab); + return sb.ToString(); + } + + protected override void GenerateFooterInformation() + { + Add("\r\n"); + } + + protected override void GenerateTitleInformation() + { + Add("PingCastle Consolidation report - "); + Add(DateTime.Now.ToString("yyyy-MM-dd")); + } + + protected override void GenerateHeaderInformation() + { + Add(@""); + Add(GetStyleSheetTheme()); + Add(GetStyleSheet()); + } + + public static string GetStyleSheet() + { + return @" + + + +"; + } + + protected override void Hook(StringBuilder sbHtml) + { + sbHtml.Replace("", @""); + } + + protected override void GenerateBodyInformation() + { + Version version = Assembly.GetExecutingAssembly().GetName().Version; + string versionString = version.ToString(4); +#if DEBUG + versionString += " Beta"; +#endif + GenerateNavigation("Consolidation", null, DateTime.Now); + GenerateAbout(@"

    Generated by Ping Castle all rights reserved

    +

    Open source components:

    +"); + Add(@" +
    + +

    Consolidation

    +

    Date: " + DateTime.Now.ToString("yyyy-MM-dd") + @" - Engine version: " + versionString + @"

    +
    +"); + GenerateContent(); + Add(@" +
    +"); + } + + void GenerateContent(string selectedTab = null) + { + Add(@" +
    +
    +
      "); + GenerateTabHeader("Active Directory Indicators", selectedTab, true); + GenerateTabHeader("Rules Matched", selectedTab); + GenerateTabHeader("Domain Information", selectedTab); + GenerateTabHeader("User Information", selectedTab); + GenerateTabHeader("Computer Information", selectedTab); + GenerateTabHeader("Admin Groups", selectedTab); + GenerateTabHeader("Trusts", selectedTab); + GenerateTabHeader("Anomalies", selectedTab); + GenerateTabHeader("Password Policies", selectedTab); + GenerateTabHeader("GPO", selectedTab); + Add(@" +
    +
    +
    +
    +
    +
    "); + + GenerateSectionFluid("Active Directory Indicators", GenerateIndicators, selectedTab, true); + GenerateSectionFluid("Rules Matched", GenerateRulesMatched, selectedTab); + GenerateSectionFluid("Domain Information", GenerateDomainInformation, selectedTab); + GenerateSectionFluid("User Information", GenerateUserInformation, selectedTab); + GenerateSectionFluid("Computer Information", GenerateComputerInformation, selectedTab); + GenerateSectionFluid("Admin Groups", GenerateAdminGroupsInformation, selectedTab); + GenerateSectionFluid("Trusts", GenerateTrustInformation, selectedTab); + GenerateSectionFluid("Anomalies", GenerateAnomalyDetail, selectedTab); + GenerateSectionFluid("Password Policies", GeneratePasswordPoliciesDetail, selectedTab); + GenerateSectionFluid("GPO", GenerateGPODetail, selectedTab); + + Add(@" +
    +
    +
    "); + } + + private void GenerateRulesMatched() + { + Add(@" +
    +
    + + + + + + + + + + + +"); + foreach (HealthcheckData data in Report) + { + foreach (HealthcheckRiskRule rule in data.RiskRules) + { + Add(@" + + + + + + + + "); + } + } + Add(@" + +
    DomainCategoryRuleScoreDescriptionRationale
    " + PrintDomain(data.Domain) + @"" + rule.Category + @"" + rule.RiskId + @"" + rule.Points + @"" + RuleSet.GetRuleDescription(rule.RiskId) + @"" + rule.Rationale + @"
    +
    +
    "); + } + + #region indicators + + private void GenerateIndicators() + { + int globalScore = 0, minscore = 0, maxscore = 0, medianscore = 0; + int sumScore = 0, num = 0; + List AllScores = new List(); + foreach (var data in Report) + { + num++; + sumScore += data.GlobalScore; + AllScores.Add(data.GlobalScore); + } + if (num > 0) + { + AllScores.Sort(); + + globalScore = sumScore / num; + minscore = AllScores[0]; + maxscore = AllScores[AllScores.Count - 1]; + if (AllScores.Count % 2 == 0) + { + var firstValue = AllScores[(AllScores.Count / 2) - 1]; + var secondValue = AllScores[(AllScores.Count / 2)]; + medianscore = (firstValue + secondValue) / 2; + } + if (AllScores.Count % 2 == 1) + { + medianscore = AllScores[(AllScores.Count / 2)]; + } + } + Add(@" + +
    +
    +
    "); + GenerateGauge(globalScore); + Add(@"
    +
    +
    +

    Average Risk Level: " + globalScore + @" / 100

    +

    Best Risk Level: " + minscore + @" / 100

    +

    Worst Risk Level: " + maxscore + @" / 100

    +

    Median Risk Level: " + medianscore + @" / 100

    +
    +
    +"); + var rules = new List(); + foreach (HealthcheckData data in Report) + { + rules.AddRange(data.RiskRules); + } + GenerateRiskModelPanel(rules, Report.Count); + GenerateIndicatorsTable(); + } + + private void GenerateIndicatorsTable() + { + Add(@" + +
    +
    + + + + + + + + + + + + +"); + foreach (HealthcheckData data in Report) + { + Add(@" + + + + + + + + + "); + } + Add(@" + +
    DomainDomain Risk LevelStale objectsPrivileged accountsTrustsAnomaliesGenerated
    " + PrintDomain(data.Domain) + @"" + data.GlobalScore + @"" + data.StaleObjectsScore + @"" + data.PrivilegiedGroupScore + @"" + data.TrustScore + @"" + data.AnomalyScore + @"" + data.GenerationDate.ToString("u") + @"
    +
    +
    +"); + } + #endregion indicators + + #region domain information + private void GenerateDomainInformation() + { + Add(@" +
    +
    + + + + + + + + + + + + + + +"); + foreach (HealthcheckData data in Report) + { + Add(@" + + + + + + + + + + + "); + } + Add(@" + + + + + + + +
    DomainNetbios NameDomain Functional LevelForest Functional LevelCreation dateNb DCEngineLevelSchema version
    "); + Add(PrintDomain(data.Domain)); + Add(@""); + AddEncoded(data.NetBIOSName); + Add(@""); + Add(ReportHelper.DecodeDomainFunctionalLevel(data.DomainFunctionalLevel)); + Add(@""); + Add(ReportHelper.DecodeForestFunctionalLevel(data.ForestFunctionalLevel)); + Add(@""); + Add(data.DomainCreation.ToString("u")); + Add(@""); + Add(data.NumberOfDC); + Add(@""); + Add(data.EngineVersion); + Add(@""); + Add(data.Level.ToString()); + Add(@""); + Add(ReportHelper.GetSchemaVersion(data.SchemaVersion)); + Add(@"
    Total" + Report.Count + @"
    +
    +
    +"); + } + #endregion domain information + + #region user + private void GenerateUserInformation() + { + Add(@" +
    +
    + + + + + + + + + + + + + + + + + + + +"); + HealthcheckAccountData total = new HealthcheckAccountData(); + foreach (HealthcheckData data in Report) + { + total.Add(data.UserAccountData); + Add(@" + + + + + + + + + + + + + + + + "); + } + Add(@" + + + + + + + + + + + + + + + + + + + +
    DomainNb User AccountsNb EnabledNb DisabledNb ActiveNb InactiveNb LockedNb pwd never ExpireNb SidHistoryNb Bad PrimaryGroupNb Password not Req.Nb Des enabled.Nb Trusted delegationNb Reversible password
    " + PrintDomain(data.Domain) + @"" + data.UserAccountData.Number + @"" + data.UserAccountData.NumberEnabled + @"" + data.UserAccountData.NumberDisabled + @"" + data.UserAccountData.NumberActive + @"" + data.UserAccountData.NumberInactive + @"" + data.UserAccountData.NumberLocked + @"" + data.UserAccountData.NumberPwdNeverExpires + @"" + data.UserAccountData.NumberSidHistory + @"" + data.UserAccountData.NumberBadPrimaryGroup + @"" + data.UserAccountData.NumberPwdNotRequired + @"" + data.UserAccountData.NumberDesEnabled + @"" + data.UserAccountData.NumberTrustedToAuthenticateForDelegation + @"" + data.UserAccountData.NumberReversibleEncryption + @"
    Total" + total.Number + @"" + total.NumberEnabled + @"" + total.NumberDisabled + @"" + total.NumberActive + @"" + total.NumberInactive + @"" + total.NumberLocked + @"" + total.NumberPwdNeverExpires + @"" + total.NumberSidHistory + @"" + total.NumberBadPrimaryGroup + @"" + total.NumberPwdNotRequired + @"" + total.NumberDesEnabled + @"" + total.NumberTrustedToAuthenticateForDelegation + @"" + total.NumberReversibleEncryption + @"
    +
    +
    +"); + } + #endregion user + + #region computer + private void GenerateComputerInformation() + { + Add(@" +
    +
    + + + + + + + + + + + + + + + +"); + HealthcheckAccountData total = new HealthcheckAccountData(); + foreach (HealthcheckData data in Report) + { + total.Add(data.ComputerAccountData); + Add(@" + + + + + + + + + + + + "); + } + Add(@" + + + + + + + + + + + + + + + +
    DomainNb Computer AccountsNb EnabledNb DisabledNb ActiveNb InactiveNb SidHistoryNb Bad PrimaryGroupNb Trusted delegationNb Reversible password
    " + PrintDomain(data.Domain) + @"" + data.ComputerAccountData.Number + @"" + data.ComputerAccountData.NumberEnabled + @"" + data.ComputerAccountData.NumberDisabled + @"" + data.ComputerAccountData.NumberActive + @"" + data.ComputerAccountData.NumberInactive + @"" + data.ComputerAccountData.NumberSidHistory + @"" + data.ComputerAccountData.NumberBadPrimaryGroup + @"" + data.ComputerAccountData.NumberTrustedToAuthenticateForDelegation + @"" + data.ComputerAccountData.NumberReversibleEncryption + @"
    Total" + total.Number + @"" + total.NumberEnabled + @"" + total.NumberDisabled + @"" + total.NumberActive + @"" + total.NumberInactive + @"" + total.NumberSidHistory + @"" + total.NumberBadPrimaryGroup + @"" + total.NumberTrustedToAuthenticateForDelegation + @"" + total.NumberReversibleEncryption + @"
    +
    +
    +"); + GenerateConsolidatedOperatingSystemList(); + } + + private string GenerateConsolidatedOperatingSystemList() + { + string output = null; + List AllOS = new List(); + Dictionary SpecificOK = new Dictionary(); + foreach (HealthcheckData data in Report) + { + if (data.OperatingSystem != null) + { + foreach (HealthcheckOSData os in data.OperatingSystem) + { + // keep only the "good" operating system (OsToInt>0) + if (OSToInt(os.OperatingSystem) > 0) + { + if (!AllOS.Contains(os.OperatingSystem)) + AllOS.Add(os.OperatingSystem); + } + else + { + // consolidate all other OS + if (!SpecificOK.ContainsKey(os.OperatingSystem)) + SpecificOK[os.OperatingSystem] = os.NumberOfOccurence; + else + SpecificOK[os.OperatingSystem] += os.NumberOfOccurence; + } + } + } + } + AllOS.Sort(OrderOS); + Add(@" +
    +
    + + + +"); + foreach (string os in AllOS) + { + Add("\r\n"); + } + Add(@" + + + +"); + // maybe not the most perfomant algorithm (n^4) but there is only a few domains to consolidate + foreach (HealthcheckData data in Report) + { + Add(@" + +"); + foreach (string os in AllOS) + { + int numberOfOccurence = -1; + if (data.OperatingSystem != null) + { + foreach (var OS in data.OperatingSystem) + { + if (OS.OperatingSystem == os) + { + numberOfOccurence = OS.NumberOfOccurence; + break; + } + } + } + Add("\r\n"); + } + Add("\r\n"); + } + Add(@" + + + + + +"); + foreach (string os in AllOS) + { + int total = 0; + foreach (HealthcheckData data in Report) + { + if (data.OperatingSystem != null) + { + foreach (var OS in data.OperatingSystem) + { + if (OS.OperatingSystem == os) + { + total += OS.NumberOfOccurence; + break; + } + } + } + } + Add(@""); + } + Add(@" + +
    Domain"); + AddEncoded(os); + Add("
    " + PrintDomain(data.Domain) + @"" + (numberOfOccurence >= 0 ? numberOfOccurence.ToString() : null) + "
    Total" + total + "
    +
    +
    "); + if (SpecificOK.Count > 0) + { + Add(@" +
    +
    + + + + + + + "); + foreach (string os in SpecificOK.Keys) + { + Add(""); + } + Add(@" + +
    Operating SystemNb
    Nb "); + AddEncoded(os); + Add(" : "); + Add(SpecificOK[os]); + Add("
    +
    +
    "); + } + return output; + } + #endregion computer + + #region admin + private void GenerateAdminGroupsInformation() + { + Add(@" +
    +
    + + + + + + + + + + + "); + + Add(@" + + + +"); + foreach (HealthcheckData data in Report) + { + foreach (HealthCheckGroupData group in data.PrivilegedGroups) + { + Add(@" + + + + + + + + + + + +"); + } + } + Add(@" + +
    DomainGroup NameNb AdminsNb EnabledNb DisabledNb InactiveNb PWd never expireNb can be delegatedNb external users
    "); + Add(PrintDomain(data.Domain)); + Add(@""); + AddEncoded(group.GroupName); + Add(@""); + Add(group.NumberOfMember); + Add(@""); + Add(group.NumberOfMemberEnabled); + Add(@""); + Add(group.NumberOfMemberDisabled); + Add(@""); + Add(group.NumberOfMemberInactive); + Add(@""); + Add(group.NumberOfMemberPwdNeverExpires); + Add(@""); + Add(group.NumberOfMemberCanBeDelegated); + Add(@""); + Add(group.NumberOfExternalMember); + Add(@"
    +
    +
    +"); + } + #endregion admin + + #region trust + private void GenerateTrustInformation() + { + List knowndomains = new List(); + GenerateSubSection("Discovered domains"); + Add(@" +
    +
    + + + + + + + + + + + + + +"); + foreach (HealthcheckData data in Report) + { + + if (!knowndomains.Contains(data.DomainFQDN)) + knowndomains.Add(data.DomainFQDN); + data.Trusts.Sort( + (HealthCheckTrustData a, HealthCheckTrustData b) + => + { + return String.Compare(a.TrustPartner, b.TrustPartner); + } + ); + + foreach (HealthCheckTrustData trust in data.Trusts) + { + if (!knowndomains.Contains(trust.TrustPartner)) + knowndomains.Add(trust.TrustPartner); + Add(@" + + + + + + + + + + +"); + } + } + Add(@" + +
    DomainTrust PartnerTypeAttributDirectionSID Filtering activeCreationIs Active ?
    " + PrintDomain(data.Domain) + @"" + PrintDomain(trust.Domain) + @"" + TrustAnalyzer.GetTrustType(trust.TrustType) + @"" + TrustAnalyzer.GetTrustAttribute(trust.TrustAttributes) + @"" + TrustAnalyzer.GetTrustDirection(trust.TrustDirection) + @"" + TrustAnalyzer.GetSIDFiltering(trust) + @"" + trust.CreationDate.ToString("u") + @"" + trust.IsActive + @"
    +
    +
    +"); + GenerateSubSection("Other discovered domains"); + Add(@" +
    +
    + + + + + + + + + + +"); + foreach (HealthcheckData data in Report) + { + foreach (HealthCheckTrustData trust in data.Trusts) + { + if (trust.KnownDomains == null) + continue; + trust.KnownDomains.Sort((HealthCheckTrustDomainInfoData a, HealthCheckTrustDomainInfoData b) + => + { + return String.Compare(a.DnsName, b.DnsName); + } + ); + foreach (HealthCheckTrustDomainInfoData di in trust.KnownDomains) + { + if (knowndomains.Contains(di.DnsName)) + continue; + knowndomains.Add(di.DnsName); + Add(@" + + + + + + + +"); + } + } + } + foreach (HealthcheckData data in Report) + { + if (data.ReachableDomains != null) + { + foreach (HealthCheckTrustDomainInfoData di in data.ReachableDomains) + { + if (knowndomains.Contains(di.DnsName)) + continue; + knowndomains.Add(di.DnsName); + Add(@" + + + + + + + +"); + } + } + } + + Add(@" + +
    FromReachable domainViaNetbiosCreation date
    "); + Add(PrintDomain(data.Domain)); + Add(@""); + AddEncoded(di.DnsName); + Add(@""); + AddEncoded(trust.TrustPartner); + Add(@""); + AddEncoded(di.NetbiosName); + Add(@""); + Add(di.CreationDate); + Add(@"
    "); + Add(PrintDomain(data.Domain)); + Add(@""); + AddEncoded(di.DnsName); + Add(@"Unknown"); + AddEncoded(di.NetbiosName); + Add(@"Unknown
    +
    +
    +"); + + // prepare a SID map to locate unknown account + SortedDictionary sidmap = new SortedDictionary(); + GenerateSubSection("SID Map"); + Add(@" +
    +
    + + + + + + + +"); + foreach (HealthcheckData data in Report) + { + if (!sidmap.ContainsKey(data.DomainFQDN) && !String.IsNullOrEmpty(data.DomainSid)) + { + sidmap.Add(data.DomainFQDN, data.DomainSid); + } + foreach (HealthCheckTrustData trust in data.Trusts) + { + if (!sidmap.ContainsKey(trust.TrustPartner) && !String.IsNullOrEmpty(trust.SID)) + { + sidmap.Add(trust.TrustPartner, trust.SID); + } + foreach (HealthCheckTrustDomainInfoData di in trust.KnownDomains) + { + if (!sidmap.ContainsKey(di.DnsName) && !String.IsNullOrEmpty(di.Sid)) + { + sidmap.Add(di.DnsName, di.Sid); + } + } + } + + } + foreach (HealthcheckData data in Report) + { + if (data.ReachableDomains != null) + { + foreach (HealthCheckTrustDomainInfoData di in data.ReachableDomains) + { + if (!sidmap.ContainsKey(di.DnsName) && !String.IsNullOrEmpty(di.Sid)) + { + sidmap.Add(di.DnsName, di.Sid); + } + } + } + } + foreach (string domain in sidmap.Keys) + { + Add(@" + + + + +"); + } + Add(@" + +
    DomainDomain SID
    "); + AddEncoded(domain); + Add(@""); + Add(sidmap[domain]); + Add(@"
    +
    +
    +"); + } + #endregion trust + + #region anomaly + private void GenerateAnomalyDetail() + { + Add(@" +
    +
    + + + + + + + + + + + +"); + foreach (HealthcheckData data in Report) + { + Add(@" + + + + + + + + +"); + } + Add(@" + +
    DomainKrbtgtAdminSDHolderDC with null sessionSmart card account not updateDate LAPS Installed
    " + PrintDomain(data.Domain) + @"" + data.KrbtgtLastChangeDate.ToString("u") + @"" + data.AdminSDHolderNotOKCount + @"" + data.DomainControllerWithNullSessionCount + @"" + data.SmartCardNotOKCount + @"" + (data.LAPSInstalled == DateTime.MaxValue ? "Never" : (data.LAPSInstalled == DateTime.MinValue ? "Not checked" : data.LAPSInstalled.ToString("u"))) + @"
    +
    +
    +"); + } + #endregion anomaly + + #region passwordpolicy + private void GeneratePasswordPoliciesDetail() + { + GenerateSubSection("Password policies"); + Add(@" +
    +
    + + + + + + + + + + + + + + + + +"); + foreach (HealthcheckData data in Report) + { + if (data.GPPPasswordPolicy != null) + { + foreach (GPPSecurityPolicy policy in data.GPPPasswordPolicy) + { + Add(@" + + + + + + + + + + + + + +"); + } + } + } + Add(@" + +
    DomainPolicy NameComplexityMax Password AgeMin Password AgeMin Password LengthPassword HistoryReversible EncryptionLockout ThresholdLockout DurationReset account counter locker after
    "); + Add(PrintDomain(data.Domain)); + Add(@""); + AddEncoded(policy.GPOName); + Add(@""); + Add(GetPSOStringValue(policy, "PasswordComplexity")); + Add(@""); + Add(GetPSOStringValue(policy, "MaximumPasswordAge")); + Add(@""); + Add(GetPSOStringValue(policy, "MinimumPasswordAge")); + Add(@""); + Add(GetPSOStringValue(policy, "MinimumPasswordLength")); + Add(@""); + Add(GetPSOStringValue(policy, "PasswordHistorySize")); + Add(@""); + Add(GetPSOStringValue(policy, "ClearTextPassword")); + Add(@""); + Add(GetPSOStringValue(policy, "LockoutBadCount")); + Add(@""); + Add(GetPSOStringValue(policy, "LockoutDuration")); + Add(@""); + Add(GetPSOStringValue(policy, "ResetLockoutCount")); + Add(@"
    +
    +
    +"); + GenerateSubSection("Screensaver policies"); + Add(@" +
    +
    + + + + + + + + + + + +"); + foreach (HealthcheckData data in Report) + { + if (data.GPPPasswordPolicy != null) + { + foreach (GPPSecurityPolicy policy in data.GPOScreenSaverPolicy) + { + string scrActive = GetPSOStringValue(policy, "ScreenSaveActive"); + string scrSecure = GetPSOStringValue(policy, "ScreenSaverIsSecure"); + string scrTimeOut = GetPSOStringValue(policy, "ScreenSaveTimeOut"); + string scrGrace = GetPSOStringValue(policy, "ScreenSaverGracePeriod"); + + Add(@" + + + + + + + + +"); + } + } + } + Add(@" + +
    DomainPolicy NameScreensaver enforcedPassword requestStart after (seconds)Grace Period (seconds)
    "); + Add(PrintDomain(data.Domain)); + Add(@""); + AddEncoded(policy.GPOName); + Add(@""); + Add(scrActive); + Add(@""); + Add(scrSecure); + Add(@""); + Add(scrTimeOut); + Add(@""); + Add(scrGrace); + Add(@"
    +
    +
    +"); + GenerateSubSection("LSA settings"); + Add(@" +
    +
    + + + + + + + + "); + foreach (HealthcheckData data in Report) + { + if (data.GPOLsaPolicy != null) + { + foreach (GPPSecurityPolicy policy in data.GPOLsaPolicy) + { + foreach (GPPSecurityPolicyProperty property in policy.Properties) + { + Add(@" + + + + + + +"); + } + } + } + } + Add(@" + +
    DomainPolicy NameSettingValue
    "); + Add(PrintDomain(data.Domain)); + Add(@""); + AddEncoded(policy.GPOName); + Add(@""); + Add(GetLinkForLsaSetting(property.Property)); + Add(@""); + Add(property.Value); + Add(@"
    +
    +
    +"); + } + #endregion passwordpolicy + + #region gpo detail + private void GenerateGPODetail() + { + GenerateSubSection("Obfuscated Password"); + Add(@" +
    +
    + + + + + + + + + + + + +"); + foreach (HealthcheckData data in Report) + { + foreach (GPPPassword password in data.GPPPassword) + { + Add(@" + + + + + + + + + +"); + } + } + Add(@" + +
    DomainGPO NamePassword originUserNamePasswordChangedOther
    "); + Add(PrintDomain(data.Domain)); + Add(@""); + AddEncoded(password.GPOName); + Add(@""); + AddEncoded(password.Type); + Add(@""); + AddEncoded(password.UserName); + Add(@""); + AddEncoded(password.Password); + Add(@""); + Add(password.Changed); + Add(@""); + AddEncoded(password.Other); + Add(@"
    +
    +
    +"); + } + #endregion gpo detail + + new string PrintDomain(DomainKey key) + { + string label = PrintDomainLabel(key); + if (GetUrlCallback == null) + return label; + string htmlData = GetUrlCallback(key, label); + if (String.IsNullOrEmpty(htmlData)) + return label; + return htmlData; + } + + string PrintDomainLabel(DomainKey key) + { + if (HasDomainAmbigousName != null) + { + if (HasDomainAmbigousName(key)) + return key.ToString(); + } + else if (Report.HasDomainAmbigiousName(key)) + return key.ToString(); + return key.DomainName; + } + } +} diff --git a/Report/ReportHealthCheckSingle.cs b/Report/ReportHealthCheckSingle.cs new file mode 100644 index 0000000..d16d1ad --- /dev/null +++ b/Report/ReportHealthCheckSingle.cs @@ -0,0 +1,1961 @@ +// +// Copyright (c) Ping Castle. All rights reserved. +// https://www.pingcastle.com +// +// Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. +// +using PingCastle.template; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using PingCastle.Rules; +using PingCastle.Healthcheck; + +namespace PingCastle.Report +{ + public class ReportHealthCheckSingle : ReportRiskControls, IPingCastleReportUser + { + + private HealthcheckData Report; + public static int MaxNumberUsersInHtmlReport = 100; + private ADHealthCheckingLicense _license; + private Version version; + + public string GenerateReportFile(HealthcheckData report, ADHealthCheckingLicense license, string filename) + { + Report = report; + _license = license; + version = new Version(Report.EngineVersion.Split(' ')[0]); + return GenerateReportFile(filename); + } + + public string GenerateRawContent(HealthcheckData report) + { + Report = report; + _license = null; + version = new Version(Report.EngineVersion.Split(' ')[0]); + sb.Length = 0; + GenerateContent(); + return sb.ToString(); + } + + protected override void GenerateTitleInformation() + { + AddEncoded(Report.DomainFQDN); + Add(" PingCastle "); + Add(Report.GenerationDate.ToString("yyyy-MM-dd")); + } + + + protected override void GenerateHeaderInformation() + { + Add(@""); + Add(GetStyleSheetTheme()); + Add(GetRiskControlStyleSheet()); + } + + protected override void GenerateBodyInformation() + { + GenerateNavigation("HealthCheck report", Report.DomainFQDN, Report.GenerationDate); + GenerateAbout(@"

    Generated by Ping Castle all rights reserved

    +

    Open source components:

    +"); + + Add(@" +
    + +

    "); + Add(Report.DomainFQDN); + Add(@" - Healthcheck analysis

    +

    Date: "); + Add(Report.GenerationDate.ToString("yyyy-MM-dd")); + Add(@" - Engine version: "); + Add(Report.EngineVersion); + Add(@"

    +"); + Add(@"
    +This report has been generated with the "); + Add(String.IsNullOrEmpty(_license.Edition) ? "Basic" : _license.Edition); + Add(@" Edition of PingCastle."); + if (String.IsNullOrEmpty(_license.Edition)) + { + Add(@" +
    Being part of a commercial package is forbidden (selling the information contained in the report).
    +If you are an auditor, you MUST purchase an Auditor license to share the development effort. +"); + } + Add(@"
    +"); + Add(@"
    +"); + GenerateContent(); + Add(@" +
    +"); + } + + void GenerateContent() + { + GenerateSection("Active Directory Indicators", () => + { + GenerateIndicators(Report, Report.AllRiskRules); + GenerateRiskModelPanel(Report.RiskRules); + }); + GenerateSection("Stale Objects", () => + { + GenerateSubIndicator("Stale Objects", Report.GlobalScore, Report.StaleObjectsScore, "It is about operations related to user or computer objects"); + GenerateIndicatorPanel("DetailStale", "Stale Objects rule details", RiskRuleCategory.StaleObjects, Report.RiskRules); + }); + GenerateSection("Privileged Accounts", () => + { + GenerateSubIndicator("Privileged Accounts", Report.GlobalScore, Report.PrivilegiedGroupScore, "It is about administrators of the Active Directory"); + GenerateIndicatorPanel("DetailPrivileged", "Privileged Accounts rule details", RiskRuleCategory.PrivilegedAccounts, Report.RiskRules); + }); + GenerateSection("Trusts", () => + { + GenerateSubIndicator("Trusts", Report.GlobalScore, Report.TrustScore, "It is about operations related to user or computer objects"); + GenerateIndicatorPanel("DetailTrusts", "Trusts rule details", RiskRuleCategory.Trusts, Report.RiskRules); + }); + GenerateSection("Anomalies analysis", () => + { + GenerateSubIndicator("Anomalies", Report.GlobalScore, Report.AnomalyScore, "It is about specific security control points"); + GenerateIndicatorPanel("DetailAnomalies", "Anomalies rule details", RiskRuleCategory.Anomalies, Report.RiskRules); + }); + GenerateSection("Domain Information", GenerateDomainInformation); + GenerateSection("User Information", GenerateUserInformation); + GenerateSection("Computer Information", GenerateComputerInformation); + GenerateSection("Admin Groups", GenerateAdminGroupsInformation); + GenerateSection("Trusts", GenerateTrustInformation); + GenerateSection("Anomalies", GenerateAnomalyDetail); + GenerateSection("Password Policies", GeneratePasswordPoliciesDetail); + GenerateSection("GPO", GenerateGPODetail); + } + + protected override void GenerateFooterInformation() + { + Add(" +"); + } + + + + + + + + #region domain info + private void GenerateDomainInformation() + { + Add(@" + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    DomainNetbios NameDomain Functional LevelForest Functional LevelCreation dateDC countSchema version
    "); + Add(Report.DomainFQDN); + Add(@""); + Add(Report.NetBIOSName); + Add(@""); + Add(ReportHelper.DecodeDomainFunctionalLevel(Report.DomainFunctionalLevel)); + Add(@""); + Add(ReportHelper.DecodeForestFunctionalLevel(Report.ForestFunctionalLevel)); + Add(@""); + Add(Report.DomainCreation); + Add(@""); + Add(Report.NumberOfDC); + Add(@""); + Add(ReportHelper.GetSchemaVersion(Report.SchemaVersion)); + Add(@"
    +
    +
    +"); + } + + #endregion domain info + + #region user info + + void AddAccountCheckHeader(bool computerView) + { + Add(@"Nb Enabled ?"); + Add(@"Nb Disabled ?"); + Add(@"Nb Active ?"); + Add(@"Nb Inactive ?"); + if (!computerView) + { + Add(@"Nb Locked ?"); + Add(@"Nb pwd never Expire ?"); + } + Add(@"Nb SidHistory ?"); + Add(@"Nb Bad PrimaryGroup ?"); + if (!computerView) + { + Add(@"Nb Password not Req. ?"); + Add(@"Nb Des enabled. ?"); + } + Add(@"Nb unconstrained delegations ?"); + Add(@"Nb Reversible password ?"); + } + + private void GenerateUserInformation() + { + GenerateSubSection("Account analysis", "useraccountanalysis"); + Add(@" +
    +
    + + + +"); + AddAccountCheckHeader(false); + Add(@" + + + + + + + +
    Nb User Accounts
    "); + Add(Report.UserAccountData.Number); + Add(@""); + Add(Report.UserAccountData.NumberEnabled); + Add(@""); + Add(Report.UserAccountData.NumberDisabled); + Add(@""); + Add(Report.UserAccountData.NumberActive); + Add(@""); + SectionList("usersaccordion", "sectioninactiveuser", Report.UserAccountData.NumberInactive, Report.UserAccountData.ListInactive); + Add(@""); + SectionList("usersaccordion", "sectionlockeduser", Report.UserAccountData.NumberLocked, Report.UserAccountData.ListLocked); + Add(@""); + SectionList("usersaccordion", "sectionneverexpiresuser", Report.UserAccountData.NumberPwdNeverExpires, Report.UserAccountData.ListPwdNeverExpires); + Add(@""); + SectionList("usersaccordion", "sectionsidhistoryuser", Report.UserAccountData.NumberSidHistory, Report.UserAccountData.ListSidHistory); + Add(@""); + SectionList("usersaccordion", "sectionbadprimarygroupuser", Report.UserAccountData.NumberBadPrimaryGroup, Report.UserAccountData.ListBadPrimaryGroup); + Add(@""); + SectionList("usersaccordion", "sectionpwdnotrequireduser", Report.UserAccountData.NumberPwdNotRequired, Report.UserAccountData.ListPwdNotRequired); + Add(@""); + SectionList("usersaccordion", "sectiondesenableduser", Report.UserAccountData.NumberDesEnabled, Report.UserAccountData.ListDesEnabled); + Add(@""); + SectionList("usersaccordion", "sectiontrusteddelegationuser", Report.UserAccountData.NumberTrustedToAuthenticateForDelegation, Report.UserAccountData.ListTrustedToAuthenticateForDelegation); + Add(@""); + SectionList("usersaccordion", "sectionreversiblenuser", Report.UserAccountData.NumberReversibleEncryption, Report.UserAccountData.ListReversibleEncryption); + Add(@"
    +
    +
    +"); + GenerateListAccount(Report.UserAccountData, "user", "usersaccordion"); + GenerateDomainSIDHistoryList(Report.UserAccountData); + } + + private void GenerateListAccount(HealthcheckAccountData data, string root, string accordion) + { + GenerateAccordion(accordion, + () => + { + if (data.ListInactive != null && data.ListInactive.Count > 0) + { + GenerateListAccountDetail(accordion, "sectioninactive" + root, "Inactive objects (Last usage > 6 months) ", data.ListInactive); + } + if (data.ListLocked != null && data.ListLocked.Count > 0) + { + GenerateListAccountDetail(accordion, "sectionlocked" + root, "Locked objects ", data.ListLocked); + } + if (data.ListPwdNeverExpires != null && data.ListPwdNeverExpires.Count > 0) + { + GenerateListAccountDetail(accordion, "sectionneverexpires" + root, "Objects with a password which never expires ", data.ListPwdNeverExpires); + } + if (data.ListSidHistory != null && data.ListSidHistory.Count > 0) + { + GenerateListAccountDetail(accordion, "sectionsidhistory" + root, "Objects having the SIDHistory populated ", data.ListSidHistory); + } + if (data.ListBadPrimaryGroup != null && data.ListBadPrimaryGroup.Count > 0) + { + GenerateListAccountDetail(accordion, "sectionbadprimarygroup" + root, "Objects having the primary group attribute changed ", data.ListBadPrimaryGroup); + } + if (data.ListPwdNotRequired != null && data.ListPwdNotRequired.Count > 0) + { + GenerateListAccountDetail(accordion, "sectionpwdnotrequired" + root, "Objects which can have an empty password ", data.ListPwdNotRequired); + } + if (data.ListDesEnabled != null && data.ListDesEnabled.Count > 0) + { + GenerateListAccountDetail(accordion, "sectiondesenabled" + root, "Objects which can use DES in kerberos authentication ", data.ListDesEnabled); + } + if (data.ListTrustedToAuthenticateForDelegation != null && data.ListTrustedToAuthenticateForDelegation.Count > 0) + { + GenerateListAccountDetail(accordion, "sectiontrusteddelegation" + root, "Objects trusted to authenticate for delegation ", data.ListTrustedToAuthenticateForDelegation); + } + if (data.ListReversibleEncryption != null && data.ListReversibleEncryption.Count > 0) + { + GenerateListAccountDetail(accordion, "sectionreversible" + root, "Objects having a reversible password ", data.ListReversibleEncryption); + } + if (data.ListDuplicate != null && data.ListDuplicate.Count > 0) + { + GenerateListAccountDetail(accordion, "sectionduplicate" + root, "Objects being duplicates ", data.ListDuplicate); + } + if (data.ListNoPreAuth != null && data.ListNoPreAuth.Count > 0) + { + GenerateListAccountDetail(accordion, "sectionnopreauth" + root, "Objects without kerberos preauthentication ", data.ListNoPreAuth); + } + }); + } + + void SectionList(string accordion, string section, int value, List list) + { + if (value > 0 && list != null && list.Count > 0) + { + Add(@""); + Add(value); + Add(@""); + } + else + { + Add(value); + } + } + + void GenerateListAccountDetail(string accordion, string id, string title, List list) + { + if (list == null) + { + return; + } + GenerateAccordionDetail(id, accordion, title, list.Count, false, () => + { + Add(@" +
    + + + + + + + + + + "); + int number = 0; + list.Sort((HealthcheckAccountDetailData a, HealthcheckAccountDetailData b) + => + { + return String.Compare(a.Name, b.Name); + } + ); + foreach (HealthcheckAccountDetailData detail in list) + { + Add(@" + + + + + "); + number++; + if (number >= MaxNumberUsersInHtmlReport) + { + break; + } + } + Add(@" + "); + if (number >= MaxNumberUsersInHtmlReport) + { + Add(""); + } + Add(@" +
    NameCreationLast logonDistinguished name
    "); + AddEncoded(detail.Name); + Add(@""); + Add((detail.CreationDate > DateTime.MinValue ? detail.CreationDate.ToString("u") : "Access Denied")); + Add(@""); + Add((detail.LastLogonDate > DateTime.MinValue ? detail.LastLogonDate.ToString("u") : "Never")); + Add(@""); + AddEncoded(detail.DistinguishedName); + Add(@"
    Output limited to "); + Add(MaxNumberUsersInHtmlReport); + Add(" items - add \"--no-enum-limit\" to remove that limit
    +
    +"); + }); + } + + private void GenerateDomainSIDHistoryList(HealthcheckAccountData data) + { + if (data.ListDomainSidHistory == null || data.ListDomainSidHistory.Count == 0) + return; + + GenerateSubSection("SID History", "sidhistory"); + Add(@" +
    +
    + + + + + + + + "); + data.ListDomainSidHistory.Sort( + (HealthcheckSIDHistoryData x, HealthcheckSIDHistoryData y) => + { + return String.Compare(x.FriendlyName, y.FriendlyName); + } + ); + foreach (HealthcheckSIDHistoryData domainSidHistory in data.ListDomainSidHistory) + { + Add(""); + } + Add(@" +
    SID History from domainFirst date seen ?Last date seen ?Count
    "); + AddEncoded(domainSidHistory.FriendlyName); + Add(""); + Add(domainSidHistory.FirstDate); + Add(""); + Add(domainSidHistory.LastDate); + Add(""); + Add(domainSidHistory.Count); + Add("
    +
    +
    "); + } + + #endregion user info + #region computer info + private void GenerateComputerInformation() + { + GenerateSubSection("Account analysis", "computeraccountanalysis"); + Add(@" +
    +
    + + + +"); + AddAccountCheckHeader(true); + Add(@" + + + + + + + + + + +
    Nb Computer Accounts
    "); + Add(Report.ComputerAccountData.Number); + Add(@""); + Add(Report.ComputerAccountData.NumberEnabled); + Add(@""); + Add(Report.ComputerAccountData.NumberDisabled); + Add(@""); + Add(Report.ComputerAccountData.NumberActive); + Add(@""); + SectionList("computersaccordion", "sectioninactivecomputer", Report.ComputerAccountData.NumberInactive, Report.ComputerAccountData.ListInactive); + Add(@""); + SectionList("computersaccordion", "sectionsidhistorycomputer", Report.ComputerAccountData.NumberSidHistory, Report.ComputerAccountData.ListSidHistory); + Add(@""); + SectionList("computersaccordion", "sectionbadprimarygroupcomputer", Report.ComputerAccountData.NumberBadPrimaryGroup, Report.ComputerAccountData.ListBadPrimaryGroup); + Add(@""); + SectionList("computersaccordion", "sectiontrusteddelegationcomputer", Report.ComputerAccountData.NumberTrustedToAuthenticateForDelegation, Report.ComputerAccountData.ListTrustedToAuthenticateForDelegation); + Add(@""); + SectionList("computersaccordion", "sectionreversiblencomputer", Report.ComputerAccountData.NumberReversibleEncryption, Report.ComputerAccountData.ListReversibleEncryption); + Add(@"
    +
    +
    +"); + GenerateListAccount(Report.ComputerAccountData, "computer", "computersaccordion"); + GenerateOperatingSystemList(); + GenerateDomainSIDHistoryList(Report.ComputerAccountData); + GenerateDCInformation(); + } + + private void GenerateOperatingSystemList() + { + GenerateSubSection("Operating Systems", "operatingsystems"); + bool oldOS = version <= new Version(2, 5, 0, 0); + if (oldOS) + { + Add(@" +
    +
    + + + + + + "); + Report.OperatingSystem.Sort( + (HealthcheckOSData x, HealthcheckOSData y) => + { + return OrderOS(x.OperatingSystem, y.OperatingSystem); + } + ); + { + foreach (HealthcheckOSData os in Report.OperatingSystem) + { + Add(""); + } + } + Add(@" +
    Operating SystemCount
    "); + AddEncoded(os.OperatingSystem); + Add(""); + Add(os.NumberOfOccurence); + Add("
    +
    +
    "); + } + else + { + Add(@" +
    +
    + + + + + "); + AddAccountCheckHeader(true); + Add(@" + + "); + Report.OperatingSystem.Sort( + (HealthcheckOSData x, HealthcheckOSData y) => + { + return OrderOS(x.OperatingSystem, y.OperatingSystem); + } + ); + { + foreach (HealthcheckOSData os in Report.OperatingSystem) + { + Add(@" + + + + + + "); + } + } + Add(@" +
    Operating SystemNb OS
    "); + Add(os.OperatingSystem); + Add(@""); + Add(os.data.Number); + Add(@""); + Add(os.data.NumberEnabled); + Add(@""); + Add(os.data.NumberDisabled); + Add(@""); + Add(os.data.NumberActive); + Add(@""); + Add(os.data.NumberInactive); + Add(@""); + Add(os.data.NumberSidHistory); + Add(@""); + Add(os.data.NumberBadPrimaryGroup); + Add(@""); + Add(os.data.NumberTrustedToAuthenticateForDelegation); + Add(@""); + Add(os.data.NumberReversibleEncryption); + Add(@"
    +
    +
    "); + } + } + + private void GenerateDCInformation() + { + if (Report.DomainControllers == null || Report.DomainControllers.Count == 0) + return; + + GenerateSubSection("Domain controllers", "domaincontrollersection"); + GenerateAccordion("domaincontrollers", () + => + { + GenerateAccordionDetail("domaincontrollersdetail", "domaincontrollers", "Domain controllers", Report.DomainControllers.Count, false, + () => + { + Add(@" +
    + + + + + + + + + + + +"); + if (version >= new Version(2, 5, 3)) + { + Add(@""); + } + Add(@" + + + "); + + int count = 0; + foreach (var dc in Report.DomainControllers) + { + count++; + Add(@" + + + + + + + + +"); + if (version >= new Version(2, 5, 3)) + { + Add(@""); + } + Add(@" +"); + } + Add(@" + +
    Domain controllerOperating SystemCreation Date ?Startup TimeUptimeOwner ?Null sessions ?SMB v1 ?Remote spooler ?
    "); + AddEncoded(dc.DCName); + Add(@""); + AddEncoded(dc.OperatingSystem); + Add(@""); + Add((dc.CreationDate == DateTime.MinValue ? "Unknown" : dc.CreationDate.ToString("u"))); + Add(@""); + Add((dc.StartupTime == DateTime.MinValue ? (dc.LastComputerLogonDate.AddDays(60) < DateTime.Now ? "Inactive?" : "Unknown") : (dc.StartupTime.AddMonths(6) < DateTime.Now ? "" + dc.StartupTime.ToString("u") + "" : dc.StartupTime.ToString("u")))); + Add(@""); + Add((dc.StartupTime == DateTime.MinValue ? "" : (DateTime.Now.Subtract(dc.StartupTime)).Days + " days")); + Add(@""); + Add((String.IsNullOrEmpty(dc.OwnerName) ? dc.OwnerSID : dc.OwnerName)); + Add(@""); + Add((dc.HasNullSession ? "YES" : "NO")); + Add(@""); + Add((dc.SupportSMB1 ? "YES" : "NO")); + Add(@""); + Add((dc.RemoteSpoolerDetected ? "YES" : "NO")); + Add("
    +
    +"); + } + ); + } + ); + + } + + + #endregion computer info + + #region admin groups + private void GenerateAdminGroupsInformation() + { + if (Report.PrivilegedGroups != null) + { + GenerateSubSection("Groups", "admingroups"); + Add(@" +
    +
    + + + + + + + + +"); + if (version >= new Version(2, 5, 2)) + { + Add(@""); + } + if (version >= new Version(2, 5, 3)) + { + Add(@""); + } + Add(@" + + + + + +"); + Report.PrivilegedGroups.Sort((HealthCheckGroupData a, HealthCheckGroupData b) + => + { + return String.Compare(a.GroupName, b.GroupName); + } + ); + foreach (HealthCheckGroupData group in Report.PrivilegedGroups) + { + Add(@" + + + + + + + + + + + + + "); + } + Add(@" + +
    Group NameNb Admins ?Nb Enabled ?Nb Disabled ?Nb Inactive ?Nb PWd never expire ?Nb Smart Card required ?Nb Service accounts ?Nb can be delegated ?Nb external users ?
    "); + if (group.Members != null && group.Members.Count > 0) + { + Add(@""); + AddEncoded(group.GroupName); + Add(""); + } + else + { + AddEncoded(group.GroupName); + } + Add(@""); + Add(group.NumberOfMember); + Add(@""); + Add(group.NumberOfMemberEnabled); + Add(@""); + Add(group.NumberOfMemberDisabled); + Add(@""); + Add(group.NumberOfMemberInactive); + Add(@""); + Add(group.NumberOfMemberPwdNeverExpires); + if (version >= new Version(2, 5, 2)) + { + Add(@""); + Add(group.NumberOfSmartCardRequired); + } + if (version >= new Version(2, 5, 3)) + { + Add(@""); + Add(group.NumberOfServiceAccount); + } + Add(@""); + Add(group.NumberOfMemberCanBeDelegated); + Add(@""); + Add(group.NumberOfExternalMember); + Add(@"
    +
    +
    +"); + foreach (HealthCheckGroupData group in Report.PrivilegedGroups) + { + if (group.Members != null && group.Members.Count > 0) + { + GenerateModalAdminGroup(GenerateModalAdminGroupIdFromGroupName(group.GroupName), group.GroupName, + () => GenerateAdminGroupsDetail(group.Members)); + } + } + } + + if (Report.AllPrivilegedMembers != null && Report.AllPrivilegedMembers.Count > 0) + { + Add(@" +
    +
    +"); + GenerateAccordion("admingroupsaccordeon", + () => + { + GenerateAccordionDetail("allprivileged", "admingroupsaccordeon", "All users in Admins groups", Report.AllPrivilegedMembers.Count, false, () => GenerateAdminGroupsDetail(Report.AllPrivilegedMembers)); + }); + Add("
    "); + } + if (Report.Delegations != null && Report.Delegations.Count > 0) + { + Add(@" +
    +
    +"); + GenerateSubSection("Delegations", "admindelegation"); + GenerateAccordion("delegationaccordeon", + () => + { + GenerateAccordionDetail("alldelegation", "delegationaccordeon", "All delegations", Report.Delegations.Count, false, GenerateDelegationDetail); + }); + Add("
    "); + } + } + private string GenerateModalAdminGroupIdFromGroupName(string groupname) + { + return "modal" + groupname.Replace(" ", "-").Replace("<",""); + } + private void GenerateModalAdminGroup(string id, string title, GenerateContentDelegate content) + { + Add(@" + +
    +
    + +
    +
    +

    " + title + @"

    + +
    +
    +"); + content(); + Add(@" +
    +
    + +
    +
    +
    +
    + +"); + } + + private void GenerateDelegationDetail() + { + Add(@" +
    +
    + + + + + + + + +"); + Report.Delegations.Sort(OrderDelegationData); + + foreach (HealthcheckDelegationData delegation in Report.Delegations) + { + int dcPathPos = delegation.DistinguishedName.IndexOf(",DC="); + string path = delegation.DistinguishedName; + if (dcPathPos > 0) + path = delegation.DistinguishedName.Substring(0, dcPathPos); + Add(@" + + + + +"); + } + Add(@" +
    DistinguishedNameAccountRight
    "); + AddEncoded(path); + Add(@""); + AddEncoded(delegation.Account); + Add(@""); + AddEncoded(delegation.Right); + Add(@"
    +
    +
    +
    "); + } + + private void GenerateAdminGroupsDetail(List members) + { + if (members != null) + { + Add(@" +
    +
    + + + + + + + +"); + if (version >= new Version(2, 5, 2)) + { + Add(@""); + } + if (version >= new Version(2, 5, 3)) + { + Add(@""); + } + Add(@" + + + + +"); + members.Sort((HealthCheckGroupMemberData a,HealthCheckGroupMemberData b) + => + { + return String.Compare(a.Name, b.Name); + } + ); + foreach (HealthCheckGroupMemberData member in members) + { + if (member.IsExternal) + { + Add(@" + + + + + + + + + + +"); + } + else + { + Add(@" + + + + + + + + + + +"); + } + } + Add(@" +
    SamAccountName ?Enabled ?Active ?Pwd never Expired ?Locked ?Smart Card required ?Service account ?Flag Cannot be delegated present ?Distinguished name ?
    "); + AddEncoded(member.Name); + Add(@"ExternalExternalExternalExternalExternal"); + if (version >= new Version(2, 5, 2)) + { + Add(@"External"); + } + if (version >= new Version(2, 5, 3)) + { + Add(@"External"); + } + AddEncoded(member.DistinguishedName); + Add(@"
    "); + AddEncoded(member.Name); + Add(@""); + Add((member.IsEnabled ? "" : "")); + Add(@""); + Add((member.IsActive ? "" : "")); + Add(@""); + Add((member.DoesPwdNeverExpires ? "YES" : "NO")); + Add(@""); + Add((member.IsLocked ? "YES" : "NO")); + Add(@""); + if (version >= new Version(2, 5, 2)) + { + Add((member.SmartCardRequired ? "YES" : "NO")); + Add(@""); + } + if (version >= new Version(2, 5, 3)) + { + Add((member.IsService ? "YES" : "NO")); + Add(@""); + } + Add((!member.CanBeDelegated ? "YES" : "NO")); + Add(@""); + AddEncoded(member.DistinguishedName); + Add(@"
    +
    +
    +"); + } + } + + // revert an OU string order to get a string orderable + // ex: OU=myOU,DC=DC => DC=DC,OU=myOU + private string GetDelegationSortKey(HealthcheckDelegationData a) + { + string[] apart = a.DistinguishedName.Split(','); + string[] apart1 = new string[apart.Length]; + for (int i = 0; i < apart.Length; i++) + { + apart1[i] = apart[apart.Length - 1 - i]; + } + return String.Join(",", apart1); + } + private int OrderDelegationData(HealthcheckDelegationData a, HealthcheckDelegationData b) + { + if (a.DistinguishedName == b.DistinguishedName) + return String.Compare(a.Account, b.Account); + return String.Compare(GetDelegationSortKey(a), GetDelegationSortKey(b)); + } + + #endregion admin groups + + #region trust + void GenerateTrustInformation() + { + List knowndomains = new List(); + GenerateSubSection("Discovered Domains", "discovereddomains"); + Add(@" +
    +
    + + + + + + + + + + + + +"); + foreach (HealthCheckTrustData trust in Report.Trusts) + { + string sid = (string.IsNullOrEmpty(trust.SID) ? "[Unknown]" : trust.SID); + string netbios = (string.IsNullOrEmpty(trust.NetBiosName) ? "[Unknown]" : trust.NetBiosName); + string sidfiltering = TrustAnalyzer.GetSIDFiltering(trust); + if (sidfiltering == "Yes") + { + sidfiltering = "" + sidfiltering + ""; + } + else if (sidfiltering == "No") + { + sidfiltering = ""+ sidfiltering + ""; + } + Add(@" + + + + + + + + +"); + } + Add(@" + +
    Trust PartnerTypeAttributDirection Bidirectional: Each domain or forest has access to the resources of the other domain or forest.
    Inbound: The other domain or forest has access to the resources of this domain or forest. This domain or forest does not have access to resources that belong to the other domain or forest.
    Outbound: This domain or forest has access to resources of the other domain or forest. The other domain or forest does not have access to the resources of this domain or forest."">?
    SID Filtering active ?Creation ?Is Active ? ?
    "); + if (GetUrlCallback == null) + { + Add(@"Netbios: "); + Add(netbios); + Add(@""">"); + AddEncoded(trust.TrustPartner); + Add(@""); + } + else + { + Add(GetUrlCallback(trust.Domain, trust.TrustPartner)); + } + Add(@""); + Add(TrustAnalyzer.GetTrustType(trust.TrustType)); + Add(@""); + Add(TrustAnalyzer.GetTrustAttribute(trust.TrustAttributes)); + Add(@""); + Add(TrustAnalyzer.GetTrustDirection(trust.TrustDirection)); + Add(@""); + Add(sidfiltering); + Add(@""); + Add(trust.CreationDate); + Add(@""); + Add((trust.IsActive ? trust.IsActive.ToString() : "False")); + Add(@"
    +
    +
    +"); + + GenerateSubSection("Reachable Domains"); + + Add(@" +
    +
    + + + + + + + + + +"); + foreach (HealthCheckTrustData trust in Report.Trusts) + { + if (trust.KnownDomains == null) + continue; + trust.KnownDomains.Sort((HealthCheckTrustDomainInfoData a, HealthCheckTrustDomainInfoData b) + => + { + return String.Compare(a.DnsName, b.DnsName); + } + ); + foreach (HealthCheckTrustDomainInfoData di in trust.KnownDomains) + { + Add(@" + + + + + +"); + } + } + if (Report.ReachableDomains != null) + { + foreach (HealthCheckTrustDomainInfoData di in Report.ReachableDomains) + { + Add(@" + + + + + +"); + } + } + + Add(@" + +
    Reachable domainViaNetbiosCreation date
    "); + if (GetUrlCallback == null) + { + AddEncoded(di.DnsName); + } + else + { + Add(GetUrlCallback(di.Domain, di.DnsName)); + } + Add(@""); + if (GetUrlCallback == null) + { + AddEncoded(trust.TrustPartner); + } + else + { + Add(GetUrlCallback(trust.Domain, trust.TrustPartner)); + } + Add(@""); + AddEncoded(di.NetbiosName); + Add(@""); + Add(di.CreationDate); + Add(@"
    "); + if (GetUrlCallback == null) + { + AddEncoded(di.DnsName); + } + else + { + Add(GetUrlCallback(di.Domain, di.DnsName)); + } + Add(@"Unknown"); + AddEncoded(di.NetbiosName); + Add(@"Unknown
    +
    +
    +"); + } + #endregion trust + + #region anomaly + private void GenerateAnomalyDetail() + { + GenerateSubSection("Backup", "backup"); + Add(@" +
    +

    The program checks the last date of the AD backup. This date is computed using the replication metadata of the attribute dsaSignature (reference).

    +

    Last backup date: " + (Report.LastADBackup == DateTime.MaxValue ? "Never" : (Report.LastADBackup == DateTime.MinValue ? "Not checked (older version of PingCastle)" : Report.LastADBackup.ToString("u"))) + @"

    +
    +"); + + GenerateSubSection("LAPS", "laps"); + Add(@" +
    +

    LAPS is used to have a unique local administrator password on all workstations / servers of the domain. +Then this password is changed at a fixed interval. The risk is when a local administrator hash is retrieved and used on other workstation in a pass-the-hash attack.

    +

    Mitigation: having a process when a new workstation is created or install LAPS and apply it through a GPO

    +

    LAPS installation date: " + (Report.LAPSInstalled == DateTime.MaxValue ? "Never" : (Report.LAPSInstalled == DateTime.MinValue ? "Not checked (older version of PingCastle)" : Report.LAPSInstalled.ToString("u"))) + @"

    +
    +"); + GenerateSubSection("Windows Event Forwarding (WEF)"); + Add(@" +
    +

    Windows Event Forwarding is a native mechanism used to collect logs on all workstations / servers of the domain. +Microsoft recommends to Use Windows Event Forwarding to help with intrusion detection +Here is the list of servers configured for WEF found in GPO

    +

    Number of WEF servers configured: " + (Report.GPOEventForwarding.Count) + @"

    +
    +"); + // wef + if (Report.GPOEventForwarding.Count > 0) + { + Add(@" +
    +
    "); + GenerateAccordion("wef", () => + { + GenerateAccordionDetail("wefPanel", "wef", "Windows Event Forwarding servers", Report.GPOEventForwarding.Count, false, () => + { + Add(@" +
    + + + + + + + + + "); + // descending sort + Report.GPOEventForwarding.Sort( + (GPOEventForwardingInfo a, GPOEventForwardingInfo b) + => + { + int comp = String.Compare(a.GPOName, b.GPOName); + if (comp == 0) + comp = (a.Order > b.Order ? 1 : (a.Order == b.Order ? 0 : -1)); + return comp; + } + ); + + foreach (var info in Report.GPOEventForwarding) + { + Add(@" + + + + + + "); + } + Add(@" +
    GPO NameOrderServer
    "); + AddEncoded(info.GPOName); + Add(@""); + Add(info.Order); + Add(@""); + AddEncoded(info.Server); + Add(@"
    +
    + "); + }); + }); + Add(@" +
    +
    +"); + } + + + // krbtgt + GenerateSubSection("krbtgt (Used for Golden ticket attacks)", "krbtgt"); + Add(@" +
    +

    The password of the krbtgt account should be changed twice every 40 days using this script

    +

    You can use the version gathered using replication metadata from two reports to guess the frequency of the password change or if the two consecutive resets has been done. Version starts at 1.

    +

    Kerberos password last changed: " + Report.KrbtgtLastChangeDate.ToString("u") + @" +version: " + Report.KrbtgtLastVersion + @" +

    +
    +"); + // adminSDHolder + GenerateSubSection("AdminSDHolder (detect temporary elevated accounts)", "admincountequalsone"); + Add(@" +
    +

    This control detects accounts which are former 'unofficial' admins. +Indeed when an account belongs to a privileged group, the attribute admincount is set. If the attribute is set without being an official member, this is suspicious. To suppress this warning, the attribute admincount of these accounts should be removed after review.

    +

    Number of accounts to review: " + + (Report.AdminSDHolderNotOKCount > 0 ? "" + Report.AdminSDHolderNotOKCount + "" : "0") + + @"

    +
    +"); + if (Report.AdminSDHolderNotOKCount > 0 && Report.AdminSDHolderNotOK != null && Report.AdminSDHolderNotOK.Count > 0) + { + GenerateAccordion("adminsdholder", () => GenerateListAccountDetail("adminsdholder", "adminsdholderpanel", "AdminSDHolder User List", Report.AdminSDHolderNotOK)); + } + + if (Report.DomainControllers != null) + { + string nullsession = null; + int countnullsession = 0; + foreach (var DC in Report.DomainControllers) + { + if (DC.HasNullSession) + { + nullsession += @"" + DC.DCName + @""; + countnullsession++; + } + } + if (countnullsession > 0) + { + GenerateSubSection("NULL SESSION (anonymous access)", "nullsession"); + Add(@" +
    +

    This control detects domain controllers which can be accessed without authentication. +Hackers can then perform a reconnaissance of the environement with only a network connectivity and no account at all.

    +

    Domain controllers vulnerable: " + countnullsession + @" +

    +
    +
    +"); + GenerateAccordion("nullsessions", () => + { + GenerateAccordionDetail("nullsessionPanel", "nullsessions", "Domain controllers with NULL SESSION Enabled", countnullsession, false, () => + { + Add(@" +
    + + + + + + + " + nullsession + @" +
    Domain Controller
    +
    +"); } + ); + } + ); + Add(@" +
    +
    +"); + } + + if (Report.SmartCardNotOK != null && Report.SmartCardNotOK.Count > 0) + { + // smart card + GenerateSubSection("Smart Card and Password", "smartcardmandatorywithnopasswordchange"); + Add(@" +
    +

    This control detects users which use only smart card and whose password hash has not been changed for at least 40 days. +Indeed, once the smart card required check is activated in the user account properties, a random password hash is set. +But this hash is not changed anymore like for users having a password whose change is controlled by password policies. +As a consequence, a capture of the hash using a memory attack tool can lead to a compromission of this account unlimited in time. +The best practice is to reset these passwords on a regular basis or to uncheck and check again the "require smart card" property to force a hash change.

    +

    Users with smart card and having their password unchanged since at least 40 days: " + + (Report.SmartCardNotOK == null ? 0 : Report.SmartCardNotOK.Count) + + @"

    +
    +"); + GenerateAccordion("anomalysmartcard", () => GenerateListAccountDetail("anomalysmartcard", "smartcard", "Smart card and Password >40 days List", Report.SmartCardNotOK)); + } + + // logon script + GenerateSubSection("Logon scripts", "logonscripts"); + Add(@" +
    +

    You can check here backdoors or typo error in the scriptPath attribute

    +
    +
    + + + + + + + +"); + // descending sort + Report.LoginScript.Sort( + (HealthcheckLoginScriptData a, HealthcheckLoginScriptData b) + => + { + return b.NumberOfOccurence.CompareTo(a.NumberOfOccurence); + } + ); + + int number = 0; + foreach (HealthcheckLoginScriptData script in Report.LoginScript) + { + Add(@" + + + + +"); + number++; + if (number >= MaxNumberUsersInHtmlReport) + { + break; + } + } + Add(@" + "); + if (number >= MaxNumberUsersInHtmlReport) + { + Add(""); + } + Add(@" +
    Script NameCount
    "); + AddEncoded(String.IsNullOrEmpty(script.LoginScript.Trim()) ? "" : script.LoginScript); + Add(@""); + Add(script.NumberOfOccurence); + Add(@"
    Output limited to "); + Add(MaxNumberUsersInHtmlReport); + Add(" items - add \"--no-enum-limit\" to remove that limit
    +
    +"); + // certificate + GenerateSubSection("Certificates", "certificates"); + Add(@" +
    +
    +

    This detects trusted certificate which can be used in man in the middle attacks or which can issue smart card logon certificates

    +

    Number of trusted certificates: " + Report.TrustedCertificates.Count + @" +

    +
    +
    +
    +"); + GenerateAccordion("trustedCertificates", () => + { + GenerateAccordionDetail("trustedCertificatesPanel", "trustedCertificates", "Trusted certificates", Report.TrustedCertificates.Count, false, () => + { + Add(@" +
    + + + + + + + + + + + + + + +"); + foreach (HealthcheckCertificateData data in Report.TrustedCertificates) + { + X509Certificate2 cert = new X509Certificate2(data.Certificate); + bool SCLogonAllowed = false; + foreach (X509Extension ext in cert.Extensions) + { + if (ext.Oid.Value == "1.3.6.1.4.1.311.20.2.2") + { + SCLogonAllowed = true; + break; + } + } + int modulesize = 0; + RSA key = null; + try + { + key = cert.PublicKey.Key as RSA; + } + catch (Exception) + { + } + if (key != null) + { + RSAParameters rsaparams = key.ExportParameters(false); + modulesize = rsaparams.Modulus.Length * 8; + } + Add(@" + + + + + + + + + + + + "); + } + Add(@" +
    SourceStoreSubjectIssuerNotBeforeNotAfterModule sizeSignature AlgSC Logon
    "); + AddEncoded(data.Source); + Add(@""); + AddEncoded(data.Store); + Add(@""); + AddEncoded(cert.Subject); + Add(@""); + AddEncoded(cert.Issuer); + Add(@""); + Add(cert.NotBefore); + Add(@""); + Add(cert.NotAfter); + Add(@""); + Add(modulesize); + Add(@""); + Add(cert.SignatureAlgorithm.FriendlyName); + Add(@""); + Add(SCLogonAllowed); + Add(@"
    +
    + "); + } + ); + } + ); + Add(@" +
    +
    +"); + } + } + #endregion anomaly + + #region password policies + + private void GeneratePasswordPoliciesDetail() + { + GenerateSubSection("Password policies", "passwordpolicies"); + Add(@" +

    Note: PSO (Password Settings Objects) will be visible only if the user which collected the information has the permission to view it.
    PSO shown in the report will be prefixed by "PSO:"

    +
    + + + + + + + + + + + + + + + +"); + if (Report.GPPPasswordPolicy != null) + { + foreach (GPPSecurityPolicy policy in Report.GPPPasswordPolicy) + { + Add(@" + + + + + + + + + + + + +"); + } + } + Add(@" + +
    Policy NameComplexityMax Password AgeMin Password AgeMin Password LengthPassword HistoryReversible EncryptionLockout ThresholdLockout DurationReset account counter locker after
    "); + AddEncoded(policy.GPOName); + Add(@""); + Add(GetPSOStringValue(policy, "PasswordComplexity")); + Add(@""); + Add(GetPSOStringValue(policy, "MaximumPasswordAge")); + Add(@""); + Add(GetPSOStringValue(policy, "MinimumPasswordAge")); + Add(@""); + Add(GetPSOStringValue(policy, "MinimumPasswordLength")); + Add(@""); + Add(GetPSOStringValue(policy, "PasswordHistorySize")); + Add(@""); + Add(GetPSOStringValue(policy, "ClearTextPassword")); + Add(@""); + Add(GetPSOStringValue(policy, "LockoutBadCount")); + Add(@""); + Add(GetPSOStringValue(policy, "LockoutDuration")); + Add(@""); + Add(GetPSOStringValue(policy, "ResetLockoutCount")); + Add(@"
    +
    +"); + GenerateSubSection("Screensaver policies"); + Add(@" +
    + + + + + + + + + + +"); + if (Report.GPOScreenSaverPolicy != null) + { + foreach (GPPSecurityPolicy policy in Report.GPOScreenSaverPolicy) + { + Add(@" + + + + + + + +"); + } + } + Add(@" + +
    Policy NameScreensaver enforcedPassword requestStart after (seconds)Grace Period (seconds)
    "); + AddEncoded(policy.GPOName); + Add(@""); + Add(GetPSOStringValue(policy, "ScreenSaveActive")); + Add(@""); + Add(GetPSOStringValue(policy, "ScreenSaverIsSecure")); + Add(@""); + Add(GetPSOStringValue(policy, "ScreenSaveTimeOut")); + Add(@""); + Add(GetPSOStringValue(policy, "ScreenSaverGracePeriod")); + Add(@"
    +
    +"); + GenerateSubSection("LSA settings", "lsasettings"); + Add(@" +
    + + + + + + + "); + if (Report.GPPPasswordPolicy != null) + { + foreach (GPPSecurityPolicy policy in Report.GPOLsaPolicy) + { + foreach (GPPSecurityPolicyProperty property in policy.Properties) + { + Add(@" + + + + + +"); + } + } + } + Add(@" + +
    Policy NameSettingValue
    "); + AddEncoded(policy.GPOName); + Add(@""); + Add(GetLinkForLsaSetting(property.Property)); + Add(@""); + Add(property.Value); + Add(@"
    +
    +"); + } + + #endregion password policies + + #region GPO + private void GenerateGPODetail() + { + GenerateSubSection("Obfuscated Passwords", "gpoobfuscatedpassword"); + Add(@" +
    +

    The password in GPO are obfuscated, not encrypted. Consider any passwords listed here as compromissed and change it immediatly.

    +
    +"); + if (Report.GPPPassword != null && Report.GPPPassword.Count > 0) + { + Add(@" +
    + + + + + + + + + + + +"); + foreach (GPPPassword password in Report.GPPPassword) + { + Add(@" + + + + + + + + + "); + } + Add(@" + +
    GPO NamePassword originUserNamePasswordChangedOther
    "); + AddEncoded(password.GPOName); + Add(@""); + AddEncoded(password.Type); + Add(@""); + AddEncoded(password.UserName); + Add(@""); + AddEncoded(password.Password); + Add(@""); + Add(password.Changed); + Add(@""); + AddEncoded(password.Other); + Add(@"
    +
    +"); + } + + GenerateSubSection("Restricted Groups"); + Add(@" +
    +

    Giving local group membership in a GPO is a way to become administrator.
    + The local admin of a domain controller can become domain administrator instantly.

    +
    +"); + if (Report.GPOLocalMembership != null && Report.GPOLocalMembership.Count > 0) + { + Report.GPOLocalMembership.Sort((GPOMembership a, GPOMembership b) => + { + int sort = String.Compare(a.GPOName, b.GPOName); + if (sort == 0) + sort = String.Compare(a.User, b.User); + if (sort == 0) + sort = String.Compare(a.MemberOf, b.MemberOf); + return sort; + } + ); + Add(@" +
    + + + + + + + + "); + + foreach (GPOMembership membership in Report.GPOLocalMembership) + { + Add(@" + + + + + +"); + } + Add(@" + +
    GPO NameUser or groupMember of
    "); + AddEncoded(membership.GPOName); + Add(@""); + AddEncoded(membership.User); + Add(@""); + AddEncoded(membership.MemberOf); + Add(@"
    +
    +"); + } + + GenerateSubSection("Privileges", "gpoprivileges"); + Add(@" +
    +

    Giving privilegdes in a GPO is a way to become administrator without being part of a group.
    + For example, SeTcbPriviledge give the right to act as SYSTEM, which has more privileges than the administrator account.

    +
    +"); + if (Report.GPPRightAssignment != null && Report.GPPRightAssignment.Count > 0) + { + Add(@" +
    + + + + + + + + "); + + foreach (GPPRightAssignment right in Report.GPPRightAssignment) + { + Add(@" + + + + + +"); + } + Add(@" + +
    GPO NamePrivilegeMembers
    "); + AddEncoded(right.GPOName); + Add(@""); + AddEncoded(right.Privilege); + Add(@""); + AddEncoded(right.User); + Add(@"
    +
    +"); + } + GenerateSubSection("GPO Login script", "gpologin"); + Add(@" +
    +

    A GPO login script is a way to force the execution of data on behalf of users.

    +
    +"); + if (Report.GPOLoginScript != null && Report.GPOLoginScript.Count > 0) + { + Add(@" +
    + + + + + + + + + + "); + + foreach (HealthcheckGPOLoginScriptData loginscript in Report.GPOLoginScript) + { + Add(@" + + + + + + + +"); + } + Add(@" + +
    GPO NameActionSourceCommand lineParameters
    "); + AddEncoded(loginscript.GPOName); + Add(@""); + AddEncoded(loginscript.Action); + Add(@""); + AddEncoded(loginscript.Source); + Add(@""); + AddEncoded(loginscript.CommandLine); + Add(@""); + AddEncoded(loginscript.Parameters); + Add(@"
    +
    +"); + } + } + #endregion GPO + } +} diff --git a/Report/ReportHelper.cs b/Report/ReportHelper.cs new file mode 100644 index 0000000..2f07248 --- /dev/null +++ b/Report/ReportHelper.cs @@ -0,0 +1,178 @@ +using PingCastle.Rules; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reflection; +using System.Text; + +namespace PingCastle.Report +{ + public class ReportHelper + { + // see https://msdn.microsoft.com/en-us/library/cc223741.aspx + // 6.1.4.2 msDS-Behavior-Version: DC Functional Level + public static string DecodeDomainFunctionalLevel(int DomainFunctionalLevel) + { + switch (DomainFunctionalLevel) + { + case 0: + return "Windows 2000"; + case 1: + return "Windows Server 2003 interim"; + case 2: + return "Windows Server 2003"; + case 3: + return "Windows Server 2008"; + case 4: + return "Windows Server 2008 R2"; + case 5: + return "Windows Server 2012"; + case 6: + return "Windows Server 2012 R2"; + case 7: + return "Windows Server 2016"; + default: + return "Unknown: " + DomainFunctionalLevel; + } + } + + // see https://msdn.microsoft.com/en-us/library/cc223743.aspx + // 6.1.4.4 msDS-Behavior-Version: Forest Functional Level + public static string DecodeForestFunctionalLevel(int ForestFunctionalLevel) + { + switch (ForestFunctionalLevel) + { + case 0: + return "Windows 2000"; + case 1: + return "Windows Server 2003 mixed"; + case 2: + return "Windows Server 2003"; + case 3: + return "Windows Server 2008"; + case 4: + return "Windows Server 2008 R2"; + case 5: + return "Windows Server 2012"; + case 6: + return "Windows Server 2012 R2"; + case 7: + return "Windows Server 2016"; + default: + return "Unknown: " + ForestFunctionalLevel; + } + } + + public static string GetSchemaVersion(int schemaVersion) + { + switch (schemaVersion) + { + case 13: + return "Windows 2000 Server"; + case 30: + return "Windows Server 2003"; + case 31: + return "Windows Server 2003 R2"; + case 44: + return "Windows Server 2008"; + case 47: + return "Windows Server 2008 R2"; + case 56: + return "Windows Server 2012"; + case 69: + return "Windows Server 2012 R2"; + case 87: + return "Windows Server 2016"; + case 88: + return "Windows Server 2019"; + case 0: + return "Not recorded at report time"; + default: + return "Unknown (" + schemaVersion + ")"; + } + } + + public static string GetEnumDescription(Enum value) + { + // Get the Description attribute value for the enum value + FieldInfo fi = value.GetType().GetField(value.ToString()); + DescriptionAttribute[] attributes = + (DescriptionAttribute[])fi.GetCustomAttributes( + typeof(DescriptionAttribute), false); + + if (attributes.Length > 0) + { + return attributes[0].Description; + } + else + { + return value.ToString(); + } + } + + public static string Encode(string stringToencode) + { + // could have use HttpUtility.HtmlEncode but not dotnet core compliant + if (string.IsNullOrEmpty(stringToencode)) return stringToencode; + + string returnString = stringToencode; + + returnString = returnString.Replace("&", "&"); + returnString = returnString.Replace("'", "'"); + returnString = returnString.Replace("\"", """); + returnString = returnString.Replace(">", ">"); + returnString = returnString.Replace("<", "<"); + + return returnString; + } + + + private static bool NeedEscape(string src, int i) + { + char c = src[i]; + return c < 32 || c == '"' || c == '\\' + // Broken lead surrogate + || (c >= '\uD800' && c <= '\uDBFF' && + (i == src.Length - 1 || src[i + 1] < '\uDC00' || src[i + 1] > '\uDFFF')) + // Broken tail surrogate + || (c >= '\uDC00' && c <= '\uDFFF' && + (i == 0 || src[i - 1] < '\uD800' || src[i - 1] > '\uDBFF')) + // To produce valid JavaScript + || c == '\u2028' || c == '\u2029' + // Escape " tags + || (c == '/' && i > 0 && src[i - 1] == '<'); + } + + public static string EscapeJsonString(string src) + { + if (String.IsNullOrEmpty(src)) + return String.Empty; + System.Text.StringBuilder sb = new System.Text.StringBuilder(); + + int start = 0; + for (int i = 0; i < src.Length; i++) + if (NeedEscape(src, i)) + { + sb.Append(src, start, i - start); + switch (src[i]) + { + case '\b': sb.Append("\\b"); break; + case '\f': sb.Append("\\f"); break; + case '\n': sb.Append("\\n"); break; + case '\r': sb.Append("\\r"); break; + case '\t': sb.Append("\\t"); break; + case '\"': sb.Append("\\\""); break; + case '\\': sb.Append("\\\\"); break; + case '/': sb.Append("\\/"); break; + default: + sb.Append("\\u"); + sb.Append(((int)src[i]).ToString("x04")); + break; + } + start = i + 1; + } + sb.Append(src, start, src.Length - start); + return sb.ToString(); + } + } +} diff --git a/Report/HealthCheckReportMapBuilder.cs b/Report/ReportMapBuilder.cs similarity index 69% rename from Report/HealthCheckReportMapBuilder.cs rename to Report/ReportMapBuilder.cs index 719a0d2..b0ed0c3 100644 --- a/Report/HealthCheckReportMapBuilder.cs +++ b/Report/ReportMapBuilder.cs @@ -4,28 +4,28 @@ // // Licensed under the Non-Profit OSL. See LICENSE file in the project root for full license information. // +using PingCastle.Data; +using PingCastle.Healthcheck; using PingCastle.template; using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; -using System.Security.Principal; using System.Text; -namespace PingCastle.Healthcheck +namespace PingCastle.Report { - public class HealthCheckReportMapBuilder : HealthCheckReportBase + public class ReportHealthCheckMapBuilder : ReportBase { - protected HealthcheckDataCollection consolidation = null; + protected PingCastleReportCollection Report = null; protected OwnerInformationReferences EntityData = null; - public HealthCheckReportMapBuilder(HealthcheckDataCollection consolidation, OwnerInformationReferences ownerInformationReferences) + public ReportHealthCheckMapBuilder(PingCastleReportCollection consolidation, OwnerInformationReferences ownerInformationReferences) { - this.consolidation = consolidation; + this.Report = consolidation; EntityData = ownerInformationReferences; FullNodeMap = true; } - public HealthCheckReportMapBuilder(HealthcheckDataCollection consolidation) : this(consolidation, null) + public ReportHealthCheckMapBuilder(PingCastleReportCollection consolidation) : this(consolidation, null) { } @@ -34,8 +34,6 @@ public HealthCheckReportMapBuilder(HealthcheckDataCollection consolidation) : th public GraphLogging Log { get; set; } - public static bool JasonOnly { get; set; } - public MigrationChecker migrationChecker { get; set; } public string CenterDomainForSimpliedGraph { get; set; } @@ -49,31 +47,34 @@ protected GraphNodeCollection Nodes get { if (_nodes == null) - _nodes = GraphNodeCollection.BuildModel(consolidation, EntityData); + _nodes = GraphNodeCollection.BuildModel(Report, EntityData); return _nodes; } } - protected override void Hook(ref string html) + protected override void Hook(StringBuilder sbHtml) { // full screen graphs - html = html.Replace("", ""); - html = html.Replace("", ""); + sbHtml.Replace("", ""); + sbHtml.Replace("", ""); } - protected override string GenerateTitleInformation() + protected override void GenerateTitleInformation() { - return "PingCastle AD Map " + DateTime.Now.ToString("yyyy-MM-dd") + " (" + Nodes.Count + " domains)"; + Add("PingCastle AD Map "); + Add(DateTime.Now.ToString("yyyy-MM-dd")); + Add(" ("); + Add(Nodes.Count); + Add(" domains)"); } - protected override string GenerateHeaderInformation() + protected override void GenerateHeaderInformation() { - StringBuilder sb = new StringBuilder(); - sb.Append(@""); - sb.Append(HealthCheckReportBase.GetStyleSheetTheme()); - sb.Append(@""); - return sb.ToString(); + Add(TemplateManager.LoadVisCss()); + Add(@""); } - protected override string GenerateBodyInformation() + protected override void GenerateBodyInformation() { - StringBuilder sb = new StringBuilder(); - sb.Append(@" - - -
    -
    - -
    -
    -

    About

    -
    -
    -
    -
    + GenerateNavigation("Active Directory map " + (FullNodeMap?"full":"simple"), null, DateTime.Now); + GenerateAbout(@"

    Generated by Ping Castle all rights reserved

    Open source components:

    -
    -
    -
    -
    - -
    -
    - -
    -
    +"); + Add(@"