fix(inventory): stop settings catalog being stored as security baselines

This commit is contained in:
Ahmed Darrazi 2026-01-08 02:26:27 +01:00
parent 16c9c7ee80
commit f61cc44ddd
6 changed files with 151 additions and 16 deletions

View File

@ -235,13 +235,9 @@ private function isEndpointSecurityConfigurationPolicy(array $policyData): bool
return false;
}
foreach ($templateReference as $value) {
if (is_string($value) && stripos($value, 'endpoint') !== false) {
return true;
}
}
$templateFamily = $templateReference['templateFamily'] ?? null;
return false;
return is_string($templateFamily) && str_starts_with(strtolower(trim($templateFamily)), 'endpointsecurity');
}
private function isSecurityBaselineConfigurationPolicy(array $policyData): bool
@ -253,17 +249,8 @@ private function isSecurityBaselineConfigurationPolicy(array $policyData): bool
}
$templateFamily = $templateReference['templateFamily'] ?? null;
if (is_string($templateFamily) && stripos($templateFamily, 'baseline') !== false) {
return true;
}
foreach ($templateReference as $value) {
if (is_string($value) && stripos($value, 'baseline') !== false) {
return true;
}
}
return false;
return is_string($templateFamily) && strcasecmp(trim($templateFamily), 'securityBaseline') === 0;
}
private function isEnrollmentStatusPageItem(array $policyData): bool

View File

@ -140,6 +140,10 @@ private function executeRun(InventorySyncRun $run, Tenant $tenant, array $normal
continue;
}
if ($this->shouldSkipPolicyForSelectedType($policyType, $policyData)) {
continue;
}
$externalId = $policyData['id'] ?? $policyData['external_id'] ?? null;
if (! is_string($externalId) || $externalId === '') {
continue;
@ -215,6 +219,55 @@ private function executeRun(InventorySyncRun $run, Tenant $tenant, array $normal
}
}
private function shouldSkipPolicyForSelectedType(string $selectedPolicyType, array $policyData): bool
{
$configurationPolicyTypes = ['settingsCatalogPolicy', 'endpointSecurityPolicy', 'securityBaselinePolicy'];
if (! in_array($selectedPolicyType, $configurationPolicyTypes, true)) {
return false;
}
return $this->resolveConfigurationPolicyType($policyData) !== $selectedPolicyType;
}
private function resolveConfigurationPolicyType(array $policyData): string
{
$templateReference = $policyData['templateReference'] ?? null;
$templateFamily = null;
if (is_array($templateReference)) {
$templateFamily = $templateReference['templateFamily'] ?? null;
}
if (is_string($templateFamily) && strcasecmp(trim($templateFamily), 'securityBaseline') === 0) {
return 'securityBaselinePolicy';
}
if ($this->isEndpointSecurityConfigurationPolicy($policyData, $templateFamily)) {
return 'endpointSecurityPolicy';
}
return 'settingsCatalogPolicy';
}
private function isEndpointSecurityConfigurationPolicy(array $policyData, ?string $templateFamily): bool
{
$technologies = $policyData['technologies'] ?? null;
if (is_string($technologies) && strcasecmp(trim($technologies), 'endpointSecurity') === 0) {
return true;
}
if (is_array($technologies)) {
foreach ($technologies as $technology) {
if (is_string($technology) && strcasecmp(trim($technology), 'endpointSecurity') === 0) {
return true;
}
}
}
return is_string($templateFamily) && str_starts_with(strtolower(trim($templateFamily)), 'endpointsecurity');
}
/**
* @return array<string, array<string, mixed>>
*/

View File

@ -0,0 +1,15 @@
# Plan: Settings Catalog Classification
## Approach
- Trace how Inventory derives **Type** and **Category** for policies.
- Fix policy-type resolution/canonicalization so Settings Catalog cannot be classified as Security Baselines.
- Add a regression test at the classification layer.
## Expected Touch Points
- `app/Services/Intune/PolicyClassificationService.php`
- Possibly Inventory mapping/display logic if category is derived elsewhere
- `tests/` regression coverage
## Rollout
- Code change affects future sync runs.
- To correct existing rows, rerun the Inventory Sync for the tenant.

View File

@ -0,0 +1,18 @@
# Spec: Settings Catalog Classification
## Problem
Some Settings Catalog policies show up as **Type: Security Baselines** and **Category: Endpoint Security** in Inventory UI.
## Goal
Ensure Settings Catalog policies are consistently classified as:
- **Type**: Settings Catalog Policy
- **Category**: Configuration
## Non-Goals
- Changing UI layout or adding new filters
- Bulk cleanup of historical data beyond what a normal re-sync updates
## Acceptance Criteria
- Classification logic never maps Settings Catalog policies to Security Baselines
- A regression test covers the misclassification scenario
- After the next Inventory Sync, affected items are stored with the correct `policy_type`/category

View File

@ -0,0 +1,7 @@
# Tasks: Settings Catalog Classification
- [x] T001 Run Spec Kit prerequisites + gather context
- [x] T002 Locate where Inventory Type/Category is derived
- [x] T003 Fix Settings Catalog misclassification in policy classification
- [x] T004 Add regression test
- [x] T005 Run targeted tests + Pint (dirty)

View File

@ -101,6 +101,61 @@ public function request(string $method, string $path, array $options = []): Grap
expect($items->first()->last_seen_run_id)->toBe($runB->id);
});
test('configuration policy inventory filtering: settings catalog is not stored as security baseline', function () {
$tenant = Tenant::factory()->create();
$settingsCatalogLookalike = [
'id' => 'pol-1',
'name' => 'Windows 11 SettingsCatalog-Test',
'@odata.type' => '#microsoft.graph.deviceManagementConfigurationPolicy',
'technologies' => ['mdm'],
'templateReference' => [
'templateDisplayName' => 'Windows Security Baseline (name only)',
],
];
$securityBaseline = [
'id' => 'pol-2',
'name' => 'Baseline Policy',
'@odata.type' => '#microsoft.graph.deviceManagementConfigurationPolicy',
'templateReference' => [
'templateFamily' => 'securityBaseline',
],
];
app()->instance(GraphClientInterface::class, fakeGraphClient([
'settingsCatalogPolicy' => [$settingsCatalogLookalike, $securityBaseline],
'securityBaselinePolicy' => [$settingsCatalogLookalike, $securityBaseline],
]));
$selection = [
'policy_types' => ['settingsCatalogPolicy', 'securityBaselinePolicy'],
'categories' => ['Configuration', 'Endpoint Security'],
'include_foundations' => false,
'include_dependencies' => false,
];
app(InventorySyncService::class)->syncNow($tenant, $selection);
expect(\App\Models\InventoryItem::query()
->where('tenant_id', $tenant->id)
->where('policy_type', 'securityBaselinePolicy')
->where('external_id', 'pol-1')
->exists())->toBeFalse();
expect(\App\Models\InventoryItem::query()
->where('tenant_id', $tenant->id)
->where('policy_type', 'settingsCatalogPolicy')
->where('external_id', 'pol-1')
->exists())->toBeTrue();
expect(\App\Models\InventoryItem::query()
->where('tenant_id', $tenant->id)
->where('policy_type', 'securityBaselinePolicy')
->where('external_id', 'pol-2')
->exists())->toBeTrue();
});
test('meta whitelist drops unknown keys without failing', function () {
$tenant = Tenant::factory()->create();