046-inventory-sync-button #47
@ -235,13 +235,9 @@ private function isEndpointSecurityConfigurationPolicy(array $policyData): bool
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($templateReference as $value) {
|
$templateFamily = $templateReference['templateFamily'] ?? null;
|
||||||
if (is_string($value) && stripos($value, 'endpoint') !== false) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return is_string($templateFamily) && str_starts_with(strtolower(trim($templateFamily)), 'endpointsecurity');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isSecurityBaselineConfigurationPolicy(array $policyData): bool
|
private function isSecurityBaselineConfigurationPolicy(array $policyData): bool
|
||||||
@ -253,17 +249,8 @@ private function isSecurityBaselineConfigurationPolicy(array $policyData): bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
$templateFamily = $templateReference['templateFamily'] ?? null;
|
$templateFamily = $templateReference['templateFamily'] ?? null;
|
||||||
if (is_string($templateFamily) && stripos($templateFamily, 'baseline') !== false) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($templateReference as $value) {
|
return is_string($templateFamily) && strcasecmp(trim($templateFamily), 'securityBaseline') === 0;
|
||||||
if (is_string($value) && stripos($value, 'baseline') !== false) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isEnrollmentStatusPageItem(array $policyData): bool
|
private function isEnrollmentStatusPageItem(array $policyData): bool
|
||||||
|
|||||||
@ -140,6 +140,10 @@ private function executeRun(InventorySyncRun $run, Tenant $tenant, array $normal
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->shouldSkipPolicyForSelectedType($policyType, $policyData)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$externalId = $policyData['id'] ?? $policyData['external_id'] ?? null;
|
$externalId = $policyData['id'] ?? $policyData['external_id'] ?? null;
|
||||||
if (! is_string($externalId) || $externalId === '') {
|
if (! is_string($externalId) || $externalId === '') {
|
||||||
continue;
|
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>>
|
* @return array<string, array<string, mixed>>
|
||||||
*/
|
*/
|
||||||
|
|||||||
15
specs/045-settingscatalog-classification/plan.md
Normal file
15
specs/045-settingscatalog-classification/plan.md
Normal 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.
|
||||||
18
specs/045-settingscatalog-classification/spec.md
Normal file
18
specs/045-settingscatalog-classification/spec.md
Normal 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
|
||||||
7
specs/045-settingscatalog-classification/tasks.md
Normal file
7
specs/045-settingscatalog-classification/tasks.md
Normal 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)
|
||||||
@ -101,6 +101,61 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
expect($items->first()->last_seen_run_id)->toBe($runB->id);
|
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 () {
|
test('meta whitelist drops unknown keys without failing', function () {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user