Prepare Data History Provider 2.7 integration update

- modularize Citrix and agent integrations and add remote desktop endpoints
- extend ticket overview, ticket links, token validation, and staged relation handling
- update configuration, licensing, project, signing, and publish artifacts
This commit is contained in:
Meik
2026-07-20 09:31:52 +02:00
parent a3357e2a36
commit ccc48c521a
62 changed files with 5267 additions and 2521 deletions

View File

@@ -1,7 +1,15 @@
using System;
using ActiveDs;
using C4IT.FASD.Base;
using C4IT.HTTP;
using C4IT.Logging;
using C4IT.MsGraph;
using C4IT_DataHistoryProvider_Base.DataSources;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.DirectoryServices.AccountManagement;
using System.DirectoryServices.ActiveDirectory;
using System.DirectoryServices.Protocols;
using System.Globalization;
using System.Linq;
@@ -9,16 +17,8 @@ using System.Net;
using System.Reflection;
using System.Security.Principal;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using ActiveDs;
using C4IT.FASD.Base;
using C4IT.HTTP;
using C4IT.Logging;
using C4IT.MsGraph;
using C4IT_DataHistoryProvider_Base.DataSources;
using Newtonsoft.Json.Linq;
using static C4IT.Logging.cLogManager;
namespace C4IT.DataHistoryProvider
@@ -50,6 +50,7 @@ namespace C4IT.DataHistoryProvider
public const string constMsGraphDeviceBitlockerRecoveryKey = "informationProtection/bitlocker/recoveryKeys?$filter=deviceId eq '{0}'";
public const string constMsGraphDeviceBitlockerRecoveryKeyDetails = "informationProtection/bitlocker/recoveryKeys/{0}?$select=key";
public const string constMsGraphDeviceLAPS = "directory/deviceLocalCredentials/{0}?$select=credentials,deviceName";
public const string constMsGraphTAP = "users/{0}/authentication/temporaryAccessPassMethods";
public const int constMsGraphManagedDevicePaging = 100;
public const string constTableNameIntuneDeviceDetails = "intune-deviceInfo";
public const string constTableNameIntuneUserDeviceCount = "intune-userDeviceCount";
@@ -93,6 +94,7 @@ namespace C4IT.DataHistoryProvider
private DateTime LastDomainsUpdate = DateTime.MinValue;
private bool DomainsUpdateRunning = false;
private Dictionary<Guid, cMsGraphBase> tenantGraphCache = new Dictionary<Guid, cMsGraphBase>();
public bool WinAuthenticationGropsResolved { get; private set; }
static cDataHistoryCollectorActiveDirectory()
{
BaseDateTime = new DateTime(1601, 1, 1, 0, 0, 0, DateTimeKind.Utc);
@@ -2053,13 +2055,17 @@ namespace C4IT.DataHistoryProvider
return true;
}
public async Task<cF4sdStagedSearchResultRelations> GetRelationsAsync(IEnumerable<cF4sdIdentityEntry> ids, enumFasdInformationClass informationClass, int age, CancellationToken token = default)
public async Task<cF4sdStagedSearchResultRelations> GetRelationsAsync(IEnumerable<cF4sdConnectorIds> ids, enumFasdInformationClass informationClass, int age, CancellationToken token = default)
{
try
{
/*
List<cF4sdConnectorIds> Ids = await _collector.getConnectorIdList(ids.ToList(), token, null, 0);
List<cF4sdApiSearchResultRelation> relations = await GetUserMobileDevicesAsync(Ids, token, null);
return new cF4sdStagedSearchResultRelations() { Relations = relations };
*/
return null;
}
catch (Exception ex)
{
@@ -2082,14 +2088,16 @@ namespace C4IT.DataHistoryProvider
var bMobileDeviceExists = Tables.Exists(v => v.Name == constTableNameIntuneMobileDeviceDetails && v.ParentCluster?.Origin == enumDataHistoryOrigin.Intune);
if (!Identities.TryGetValue(enumFasdInformationClass.Computer, out var computer) && computer?.intuneId != Guid.Empty)
if (!Identities.TryGetValue(enumFasdInformationClass.User, out var user) && user?.intuneId != Guid.Empty && user?.tenantId != Guid.Empty)
bUserDeviceExists = false;
if (!Identities.TryGetValue(enumFasdInformationClass.Computer, out var computer) && computer?.intuneId != Guid.Empty && user?.tenantId != Guid.Empty)
bDeviceExists = false;
if(!Identities.TryGetValue(enumFasdInformationClass.MobileDevice, out var mobile) && mobile?.Id != Guid.Empty)
if (!Identities.TryGetValue(enumFasdInformationClass.MobileDevice, out var mobile) && mobile?.Id != Guid.Empty)
bMobileDeviceExists = false;
if(!Identities.TryGetValue(enumFasdInformationClass.User, out var user) && user?.intuneId != Guid.Empty)
bUserDeviceExists = false;
if (!(bDeviceExists || bMobileDeviceExists || bUserDeviceExists))
@@ -2111,8 +2119,8 @@ namespace C4IT.DataHistoryProvider
}
else
listTasks.Add(Task.Run<List<cF4SDHealthCardRawData.cHealthCardTable>>(async () => { return await GetIntuneDeviceTableAsync(computer.intuneId, computer.tenantId, requestInfo, LogDeep + 1, Token); }));
listTasks.Add(Task.Run<List<cF4SDHealthCardRawData.cHealthCardTable>>(async () => { return await GetIntuneDeviceTableAsync(computer.intuneId, user.tenantId, requestInfo, LogDeep + 1, Token); }));
if (bMobileDeviceExists)
if (CacheId == null && !instantly)
{
@@ -2237,20 +2245,22 @@ namespace C4IT.DataHistoryProvider
if (MsGraph == null)
return null;
var retVal = new cF4SDHealthCardRawData.cHealthCardTable()
{
Name = constTableNameIntuneDeviceDetails,
InformationClass = enumFasdInformationClass.Computer,
Origin = enumDataHistoryOrigin.Intune,
IsStatic = true,
TableType = eDataHistoryTableType.Static
};
var dicVals = new Dictionary<string, object>();
var res = await MsGraph.RequestAsync(strUrl, UseBeta: true, retryForbidden: false);
if (res != null)
{
JObject deviceInfos = res.Result;
var retVal = new cF4SDHealthCardRawData.cHealthCardTable()
{
Name = constTableNameIntuneDeviceDetails,
InformationClass = enumFasdInformationClass.Computer,
Origin = enumDataHistoryOrigin.Intune,
IsStatic = true,
TableType = eDataHistoryTableType.Static
};
var dicVals = new Dictionary<string, object>();
var vals = deviceInfos.Children();
if (cLogManager.DefaultLogger.IsDebug) LogEntry("SpecialDebug GetIntuneMainTableAsync #3");
@@ -2290,6 +2300,16 @@ namespace C4IT.DataHistoryProvider
else
{
LogEntry($"Could not get detailed device information from intune with id {IntuneId}. Device content is empty.", LogLevels.Warning);
foreach (var colInfo in IntuneTableDetails.Columns.Values)
{
try
{
var _col = new cF4SDHealthCardRawData.cHealthCardTableColumn(retVal) { ColumnName = colInfo.Name, Values = new List<object>(1) { } };
retVal.Columns[colInfo.Name] = _col;
}
catch { }
}
return new List<cF4SDHealthCardRawData.cHealthCardTable>(1) { retVal };
}
}
catch (Exception E)
@@ -2326,17 +2346,19 @@ namespace C4IT.DataHistoryProvider
if (cLogManager.DefaultLogger.IsDebug) LogEntry("SpecialDebug GetIntuneUserDeviceCountTableAsync #3");
var res = await MsGraph.RequestAsync(strUrl, UseBeta: false, retryForbidden: false);
if (cLogManager.DefaultLogger.IsDebug) LogEntry("SpecialDebug GetIntuneUserDeviceCountTableAsync #4");
var retVal = new cF4SDHealthCardRawData.cHealthCardTable()
{
Name = constTableNameIntuneUserDeviceCount,
InformationClass = enumFasdInformationClass.User,
Origin = enumDataHistoryOrigin.Intune,
IsStatic = true,
TableType = eDataHistoryTableType.Static
};
if (res != null)
{
JObject deviceInfos = res.Result;
var retVal = new cF4SDHealthCardRawData.cHealthCardTable()
{
Name = constTableNameIntuneUserDeviceCount,
InformationClass = enumFasdInformationClass.User,
Origin = enumDataHistoryOrigin.Intune,
IsStatic = true,
TableType = eDataHistoryTableType.Static
};
var vals = deviceInfos.Children();
@@ -2405,6 +2427,16 @@ namespace C4IT.DataHistoryProvider
else
{
LogEntry($"Could not get detailed DeviceCount information from intune with id {Userid}. DeviceCount content is empty.", LogLevels.Warning);
foreach (var colInfo in IntuneTableDetails.Columns.Values)
{
try
{
var _col = new cF4SDHealthCardRawData.cHealthCardTableColumn(retVal) { ColumnName = colInfo.Name, Values = new List<object>(1) { } };
retVal.Columns[colInfo.Name] = _col;
}
catch { }
}
return new List<cF4SDHealthCardRawData.cHealthCardTable>(1) { retVal };
}
}
catch (Exception E)
@@ -2437,26 +2469,25 @@ namespace C4IT.DataHistoryProvider
var MsGraph = await GetGraphForTenantAsync(TenantId, true);
if (MsGraph == null)
return null;
var retValList = new List<cF4SDHealthCardRawData.cHealthCardDetailsTable>();
var res = await MsGraph.RequestAsync(strUrl, UseBeta: false, retryForbidden: false);
var retVal = new cF4SDHealthCardRawData.cHealthCardDetailsTable()
{
Name = constTableNameIntuneUserDeviceCountDetail,
Columns = IntuneTableDetails.Columns.Values.Select(v => v.Name).ToList(),
Values = new Dictionary<int, List<object[]>>()
};
if (res != null)
{
JObject deviceInfos = res.Result;
var retValList = new List<cF4SDHealthCardRawData.cHealthCardDetailsTable>();
var vals = deviceInfos["value"] as JArray;
if (vals != null)
{
var retVal = new cF4SDHealthCardRawData.cHealthCardDetailsTable()
{
Name = constTableNameIntuneUserDeviceCountDetail,
Columns = IntuneTableDetails.Columns.Values.Select(v => v.Name).ToList(),
Values = new Dictionary<int, List<object[]>>()
};
int index = 0;
@@ -2488,6 +2519,8 @@ namespace C4IT.DataHistoryProvider
else
{
LogEntry($"Could not get detailed DeviceCount information from intune with id {Userid}. DeviceCount content is empty.", LogLevels.Warning);
retValList.Add(retVal);
return retValList;
}
}
return null;
@@ -2512,25 +2545,25 @@ namespace C4IT.DataHistoryProvider
{
string userIds = string.Join(",", UserId.Where(u => !string.IsNullOrWhiteSpace(u?.intuneId.ToString())).Select(u => u.intuneId));
var tenantId = UserId?.FirstOrDefault(u => u?.tenantId != Guid.Empty)?.tenantId ?? Guid.Empty;
var tenantId = UserId?.FirstOrDefault(u => u?.tenantId != Guid.Empty)?.tenantId ?? Guid.Empty;
var strUrl = string.Format(constMsGraphManagedMobileDeviceList, userIds);
var MsGraph = await GetGraphForTenantAsync(tenantId, true);
if (MsGraph == null)
return null;
var Result = await MsGraph.RequestListAsync(strUrl, UseBeta: false, retryForbidden: false);
if (Result != null == Result.Count > 0)
{
var searchResult = new List<cF4sdApiSearchResultRelation>(Result.Count);
foreach (var Entry in Result)
{
var endDate = DateTime.MinValue;
if (!DateTime.TryParse(Entry.Result.lastSyncDateTime?.ToString(), out endDate))
endDate = DateTime.UtcNow;
var searchInfo = new cF4sdApiSearchResultRelation()
{
Type = enumF4sdSearchResultClass.MobileDevice,
@@ -2677,6 +2710,9 @@ namespace C4IT.DataHistoryProvider
case "GetLAPS":
result = await GetLAPS(jsonRequest.Identities, requestInfo, CancellationToken.None);
break;
case "GetTAP":
result = await GetTAP(jsonRequest.Identities, requestInfo, CancellationToken.None);
break;
case "GetBitlockerKeyTest":
result = await GetBitlockerKeyTest(jsonRequest.Identities, requestInfo, CancellationToken.None);
break;
@@ -2688,7 +2724,7 @@ namespace C4IT.DataHistoryProvider
}
public override async Task<List<string>> GetQuickActionList()
{
var HasIntuneConfig = (Collector.InfrastructureConfig.AzureTenants?.Values.Any(t => t.ScanIntuneDevices) == true);
if (HasIntuneConfig)
@@ -2699,8 +2735,9 @@ namespace C4IT.DataHistoryProvider
"Get Managed Apps",
"Get BitlockerKey",
"Get LAPS",
"Get TAP",
"GetLapsKeyTest",
};
};
return await Task.FromResult(quickactions);
}
var result = new List<string>();
@@ -2827,6 +2864,40 @@ namespace C4IT.DataHistoryProvider
return null;
}
public async Task<dynamic> GetTAP(List<cF4sdIdentityEntry> identities, cF4sdWebRequestInfo requestInfo, CancellationToken Token)
{
var Ids = await Collector.getConntectorIds(identities, Token, requestInfo, 1);
if (!Ids.TryGetValue(enumFasdInformationClass.User, out var _ids))
return null;
var strUrl = string.Format(constMsGraphTAP, _ids.intuneId);
var MsGraph = await GetGraphForTenantAsync(_ids.tenantId, true);
if (MsGraph == null)
return null;
var IntuneConfig = Collector?.GetGlobalConfig()?.IntuneConfiguration;
var body = new Dictionary<string, object>()
{
{ "isUsableOnce", IntuneConfig?.TAPIsUsableOnce ?? cF4sdIntuneConfig.DefaultTAPIsUsableOnce},
{ "lifetimeInMinutes", IntuneConfig?.TAPLifeTimeInMinutes ?? cF4sdIntuneConfig.DefaultTAPLifeTimeInMinutes}
};
var Result = await MsGraph.RequestAsync(strUrl, UseBeta: false, eHttpMethod.post, JsonData: body, retryForbidden: false);
if (Result != null)
{
var _resultJson = Result.Result as JObject;
if (_resultJson != null)
{
return _resultJson;
}
}
return null;
}
public async Task<dynamic> GetManagedApps(List<cF4sdIdentityEntry> identities, cF4sdWebRequestInfo requestInfo, CancellationToken Token)
{
var Ids = await Collector.getConntectorIds(identities, Token, requestInfo, 1);
@@ -3131,6 +3202,9 @@ namespace C4IT.DataHistoryProvider
if (Collector.InfrastructureConfig.AzureTenants != null)
await DoAzureScanAsync(requestInfo, LogDeep + 1, Token);
if (Collector.InfrastructureConfig.AzureTenants?.Values.Any(t => t.ScanIntuneDevices) == true)
await DoScanIntuneAsync(requestInfo, LogDeep + 1, Token);
await Collector.SetLastScanTime("AdScan-all", DateTime.UtcNow, requestInfo, LogDeep + 1, Token);
return true;
@@ -3443,7 +3517,7 @@ namespace C4IT.DataHistoryProvider
if (Table?.ParentCluster?.Origin == enumDataHistoryOrigin.Intune)
return true;
return false;
}
}
private object ConvertToF4sdType(object objVal, enumFasdValueType ValueType)
{
@@ -3691,68 +3765,26 @@ namespace C4IT.DataHistoryProvider
MethodBase CM = null; if (cLogManager.DefaultLogger.IsDebug) { CM = MethodBase.GetCurrentMethod(); LogMethodBegin(CM); }
try
{
if (Collector?.InfrastructureConfig?.Authorisation?.MembershipGroups?.GroupsAd == null)
if (Collector?.InfrastructureConfig?.Authorisation?.MembershipGroups?.GroupsAd is null)
return;
LogEntry("Resolving AD groups SIDs...");
var _lstDomains = Collector.InfrastructureConfig.Authorisation.MembershipGroups.GroupsAd;
var domains = Collector.InfrastructureConfig.Authorisation.MembershipGroups.GroupsAd.Values;
LogEntry("Resolving AD groups SIDs finished");
foreach (var _domain in _lstDomains.Values)
bool hasResolved = true;
foreach (cF4SDMembershipGroupsAd domain in domains)
{
try
{
LogEntry($"Start processing domain");
LogEntry($"Processing domain {_domain?.Domain?.FQDN}");
PrincipalContext PC;
if (_domain.Domain.Credential == null)
PC = new PrincipalContext(ContextType.Domain, _domain.Domain.FQDN);
else
PC = new PrincipalContext(ContextType.Domain, _domain.Domain.FQDN, _domain.Domain.Credential.User, _domain.Domain.Credential.NwCredential.Password);
LogEntry($"Principal context successful created");
foreach (var _group in _domain.Values)
{
try
{
LogEntry($"Start processing group.");
LogEntry($"Group: {_group.Name}.");
var GI = GroupPrincipal.FindByIdentity(PC, IdentityType.SamAccountName, _group.Account);
LogEntry($"Group principal found");
LogEntry($"GI.SID: {GI.Sid.ToString()}");
if (GI != null)
if (GI.Sid != null)
{
_group.SID = GI.Sid.ToString();
LogEntry($"AD group '{_group.Account}' in domain '{_domain.Domain.FQDN}' resolved. SID = {_group.SID}");
}
if (_group.SID == null)
LogEntry($"Could not resolve AD group '{_group.Account}' in domain '{_domain.Domain.FQDN}'", LogLevels.Warning);
}
catch (Exception E)
{
LogException(E);
}
finally
{
LogEntry($"End processing group.");
}
}
}
catch (Exception E)
{
LogException(E);
}
finally
{
LogEntry($"End processing domain");
}
hasResolved = hasResolved && TryProcessDomain(domain);
}
WinAuthenticationGropsResolved = hasResolved;
}
catch (Exception E)
catch (Exception ex)
{
LogException(E);
LogException(ex);
WinAuthenticationGropsResolved = false;
}
finally
{
@@ -3760,6 +3792,80 @@ namespace C4IT.DataHistoryProvider
}
}
private bool TryProcessDomain(cF4SDMembershipGroupsAd domain)
{
try
{
bool hasProcessed = true;
if (domain?.Domain is null)
return hasProcessed;
LogEntry($"Start processing domain");
string domainName = domain.Domain.FQDN;
LogEntry($"Processing domain {domainName}");
PrincipalContext principalContext;
if (domain.Domain.Credential == null)
principalContext = new PrincipalContext(ContextType.Domain, domainName);
else
principalContext = new PrincipalContext(ContextType.Domain, domainName, domain.Domain.Credential.User, domain.Domain.Credential.NwCredential.Password);
LogEntry($"Principal context successful created");
foreach (var group in domain.Values)
{
hasProcessed = hasProcessed && TryProcessMembershipGroup(group, principalContext, domainName);
}
return hasProcessed;
}
catch (Exception ex)
{
LogException(ex);
return false;
}
finally
{
LogEntry($"End processing domain");
}
}
private bool TryProcessMembershipGroup(cF4SDMembershipGroupAd group, PrincipalContext principalContext, string domainName)
{
try
{
if (group is null)
return true;
LogEntry($"Start processing group.");
LogEntry($"Group: {group.Name}.");
var groupIdentity = GroupPrincipal.FindByIdentity(principalContext, IdentityType.SamAccountName, group.Account);
LogEntry($"Group principal found");
LogEntry($"GI.SID: {groupIdentity.Sid}");
if (groupIdentity?.Sid != null)
{
group.SID = groupIdentity.Sid.ToString();
LogEntry($"AD group '{group.Account}' in domain '{domainName}' resolved. SID = {group.SID}");
}
if (group.SID == null)
LogEntry($"Could not resolve AD group '{group.Account}' in domain '{domainName}'", LogLevels.Warning);
return true;
}
catch (Exception ex)
{
LogException(ex);
return false;
}
finally
{
LogEntry($"End processing group.");
}
}
static public Guid ConvertAzureSidToGuid(string sid)
{
if (!sid.StartsWith("S-1-12-1-"))