Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## Unreleased

### Fixed

- Rebuild the three Windows Server 2025 role baselines without removing or
reordering any controls: Member Server remains 320 resources, Domain
Controller 321, and Workgroup Member 296. The full overlays use canonical
`REG_*` Registry contracts, writable CSP `Config` paths, five additional
CSP-to-Registry mappings, and CEL expressions equivalent to the existing
compliance schemas.
- Export `Microsoft.OSConfig` MOFs with a portable `ModuleVersion = "0.0.0"`
placeholder, one shared correlation-group GUID, and canonical `Value`,
`ValueName`, and `ValueType` fields. Packaging instructions resolve the
placeholder to each customer's installed module version before creating the
Machine Configuration ZIP.

## [0.3.95-author.1] - 2026-07-27

> `mac-v0.3.95-author.1` is the current macOS Author tagged source. Its
Expand Down
139 changes: 117 additions & 22 deletions apps/desktop/src/data/baseline-catalog.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,37 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { describe, it, expect } from 'vitest';
import { BASELINE_CATALOG } from './baseline-catalog';
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import yaml from "js-yaml";
import { BASELINE_CATALOG } from "./baseline-catalog";

interface BaselineResource {
name: string;
type: string;
properties?: {
resource?: {
type?: string;
properties?: Record<string, unknown>;
};
expression?: string;
schema?: unknown;
};
}

const BASELINE_DIR = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"../../../../public/_baselines",
);

function loadBaseline(filename: string): BaselineResource[] {
const document = yaml.load(readFileSync(path.join(BASELINE_DIR, filename), "utf8")) as {
resources?: BaselineResource[];
};
return document.resources ?? [];
}

/**
* Regression coverage for the WS2025 standalone-baseline fix.
Expand All @@ -14,52 +43,118 @@ import { BASELINE_CATALOG } from './baseline-catalog';
* in the corrected counts and confirms no "Source" link is rendered for
* these three baselines, while leaving every other catalog entry untouched.
*/
describe('BASELINE_CATALOG — WS2025 standalone-provider fix', () => {
const ws2025MemberServer = BASELINE_CATALOG.find((b) => b.id === 'ws2025-member-server');
const ws2025DomainController = BASELINE_CATALOG.find((b) => b.id === 'ws2025-domain-controller');
const ws2025WorkgroupMember = BASELINE_CATALOG.find((b) => b.id === 'ws2025-workgroup-member');
describe("BASELINE_CATALOG — WS2025 standalone-provider fix", () => {
const ws2025MemberServer = BASELINE_CATALOG.find((b) => b.id === "ws2025-member-server");
const ws2025DomainController = BASELINE_CATALOG.find((b) => b.id === "ws2025-domain-controller");
const ws2025WorkgroupMember = BASELINE_CATALOG.find((b) => b.id === "ws2025-workgroup-member");

it('reports the corrected resource count for ws2025-member-server (320)', () => {
it("reports the corrected resource count for ws2025-member-server (320)", () => {
expect(ws2025MemberServer?.resourceCount).toBe(320);
});

it('reports the corrected resource count for ws2025-domain-controller (321)', () => {
it("reports the corrected resource count for ws2025-domain-controller (321)", () => {
expect(ws2025DomainController?.resourceCount).toBe(321);
});

it('reports the corrected resource count for ws2025-workgroup-member (296)', () => {
it("reports the corrected resource count for ws2025-workgroup-member (296)", () => {
expect(ws2025WorkgroupMember?.resourceCount).toBe(296);
});

it('has no githubUrl for ws2025-member-server (no Source button)', () => {
it("has no githubUrl for ws2025-member-server (no Source button)", () => {
expect(ws2025MemberServer?.githubUrl).toBeUndefined();
});

it('has no githubUrl for ws2025-domain-controller (no Source button)', () => {
it("has no githubUrl for ws2025-domain-controller (no Source button)", () => {
expect(ws2025DomainController?.githubUrl).toBeUndefined();
});

it('has no githubUrl for ws2025-workgroup-member (no Source button)', () => {
it("has no githubUrl for ws2025-workgroup-member (no Source button)", () => {
expect(ws2025WorkgroupMember?.githubUrl).toBeUndefined();
});

it('leaves manifestUrl and scenarioName untouched for the three WS2025 baselines', () => {
expect(ws2025MemberServer?.manifestUrl).toBe('/_baselines/ws2025-member-server.osc.yaml');
expect(ws2025MemberServer?.scenarioName).toBe('SecurityBaseline/WS2025/MemberServer');
it("leaves manifestUrl and scenarioName untouched for the three WS2025 baselines", () => {
expect(ws2025MemberServer?.manifestUrl).toBe("/_baselines/ws2025-member-server.osc.yaml");
expect(ws2025MemberServer?.scenarioName).toBe("SecurityBaseline/WS2025/MemberServer");

expect(ws2025DomainController?.manifestUrl).toBe('/_baselines/ws2025-domain-controller.osc.yaml');
expect(ws2025DomainController?.scenarioName).toBe('SecurityBaseline/WS2025/DomainController');
expect(ws2025DomainController?.manifestUrl).toBe(
"/_baselines/ws2025-domain-controller.osc.yaml",
);
expect(ws2025DomainController?.scenarioName).toBe("SecurityBaseline/WS2025/DomainController");

expect(ws2025WorkgroupMember?.manifestUrl).toBe('/_baselines/ws2025-workgroup-member.osc.yaml');
expect(ws2025WorkgroupMember?.scenarioName).toBe('SecurityBaseline/WS2025/WorkgroupMember');
expect(ws2025WorkgroupMember?.manifestUrl).toBe("/_baselines/ws2025-workgroup-member.osc.yaml");
expect(ws2025WorkgroupMember?.scenarioName).toBe("SecurityBaseline/WS2025/WorkgroupMember");
});

it('positive control: an untouched baseline still has its githubUrl (Source button preserved)', () => {
it("positive control: an untouched baseline still has its githubUrl (Source button preserved)", () => {
// ws2025-secured-core is a sibling WS2025 baseline that is NOT part of
// this fix — its upstream link must still render a Source button.
const securedCore = BASELINE_CATALOG.find((b) => b.id === 'ws2025-secured-core');
const securedCore = BASELINE_CATALOG.find((b) => b.id === "ws2025-secured-core");
expect(securedCore?.githubUrl).toBe(
'https://github.com/microsoft/osconfig/blob/main/security/ws2025/secured_core.osc.yaml',
"https://github.com/microsoft/osconfig/blob/main/security/ws2025/secured_core.osc.yaml",
);
});
});

describe("WS2025 full-overlay invariants", () => {
const profiles = [
["ws2025-member-server.osc.yaml", 320],
["ws2025-domain-controller.osc.yaml", 321],
["ws2025-workgroup-member.osc.yaml", 296],
] as const;

it.each(profiles)("preserves every resource in %s", (filename, count) => {
const resources = loadBaseline(filename);
expect(resources).toHaveLength(count);
expect(new Set(resources.map((resource) => resource.name)).size).toBe(count);

const cspPaths: string[] = [];
for (const wrapper of resources) {
expect(wrapper.type).toBe("Microsoft.OSConfig/Test");
expect(wrapper.properties?.schema).toBeUndefined();
expect(wrapper.properties?.expression).toEqual(expect.any(String));

const inner = wrapper.properties?.resource;
if (inner?.type === "Microsoft.Windows/Registry") {
expect(inner.properties?.valueType).toMatch(/^REG_/);
expect(inner.properties?.keyPath).toMatch(/^(?:HKLM|HKCU|HKCR|HKU|HKCC|HKEY_[A-Z_]+):\\/i);
}
if (inner?.type === "Microsoft.Windows/CSP") {
cspPaths.push(String(inner.properties?.path ?? ""));
}
}

expect(cspPaths).toHaveLength(5);
expect(cspPaths.every((cspPath) => cspPath.includes("/Policy/Config/"))).toBe(true);
expect(cspPaths.some((cspPath) => cspPath.includes("/Policy/Result/"))).toBe(false);
});

it("keeps the reviewed CSP-to-provider repairs", () => {
const resources = loadBaseline("ws2025-workgroup-member.osc.yaml");
const byName = new Map(resources.map((resource) => [resource.name, resource]));
const dedicated = [
"AuditBackupAndRestorePrivilege",
"DmaGuardDeviceEnumerationPolicy",
"DeviceGuardRequirePlatformSecurityFeatures",
"RecoveryConsoleAllowFloppyCopyAndAllDrives",
"SmartCardRemovalBehavior",
];

for (const name of dedicated) {
expect(byName.get(name)?.properties?.resource?.type).toBe("Microsoft.Windows/Registry");
}
expect(
byName.get("AuditBackupAndRestorePrivilege")?.properties?.resource?.properties,
).toMatchObject({
keyPath: "HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Lsa",
valueName: "FullPrivilegeAuditing",
valueType: "REG_BINARY",
value: "AA==",
});
expect(
byName.get("NetBTNodeTypeConfiguration")?.properties?.resource?.properties,
).toMatchObject({ value: 0 });
expect(
byName.get("ServerSPNTargetNameValidationLevel")?.properties?.resource?.properties,
).toMatchObject({ value: 0 });
});
});
11 changes: 11 additions & 0 deletions docs/src/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@
A concise release history for ConfigForge. Newer entries use the
shipped semver tag; older entries summarize the foundational work by theme.

## Unreleased

- **Windows Server 2025 baselines:** Rebuilt the full Member Server (320),
Domain Controller (321), and Workgroup Member (296) profiles without losing
controls or changing their order. Registry resources use canonical contracts,
writable CSP paths replace read-only paths, and existing schema semantics are
represented as CEL expressions.
- **Machine Configuration MOF export:** Exported resources use a portable
`Microsoft.OSConfig` `0.0.0` placeholder that is resolved at package time,
plus the correlation and typed value fields required by the DSC resource.

## macOS Author v0.3.95-author.1 — 2026-07-27

`mac-v0.3.95-author.1` is the current macOS Author tagged source. Its
Expand Down
40 changes: 29 additions & 11 deletions docs/src/user-guide/manifest-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,38 +126,56 @@ installed to bundle it into the package:

```powershell
Install-Module GuestConfiguration -Scope AllUsers -Force
# OSConfig resource module (any 1.2.0 or later - the MOF is not
# pinned to a specific version, so it binds to whatever is installed)
# The exported MOF carries a portable 0.0.0 placeholder. Resolve it to
# the newest module installed on the packaging machine before packaging.
Install-Module Microsoft.OSConfig -Scope AllUsers -Repository PSGallery -Force
```

Then build and publish:

```powershell
$InstalledOSConfig = Get-Module -ListAvailable Microsoft.OSConfig |
Sort-Object Version -Descending |
Select-Object -First 1
$ResolvedMof = '.\MySecurityBaseline.resolved.mof'
(Get-Content '.\MySecurityBaseline.mof' -Raw).Replace(
'ModuleVersion = "0.0.0";',
"ModuleVersion = `"$($InstalledOSConfig.Version)`";"
) | Set-Content $ResolvedMof -Encoding utf8

# MOF to Machine Configuration package (.zip)
New-GuestConfigurationPackage `
-Name MySecurityBaseline `
-Configuration .\MySecurityBaseline.mof `
-Type AuditAndSet
-Configuration $ResolvedMof `
-Type Audit `
-Path .\package

# Publish the package, then generate the Azure Policy definition
Publish-GuestConfigurationPackage -Path .\MySecurityBaseline\MySecurityBaseline.zip
# Upload the returned ZIP to Azure Storage with Set-AzStorageBlobContent,
# then pass its read-only URI to the policy generator.
New-GuestConfigurationPolicy `
-PolicyId <new-guid> `
-ContentUri <published-package-uri> `
-ContentUri <package-uri> `
-DisplayName 'My Security Baseline' `
-Path .\policy
-Path .\policy `
-Platform Windows `
-PolicyVersion 1.0.0 `
-Mode Audit
```

Then assign the generated definition in Azure Policy. Use this route
when you want to own the packaging and publishing step - custom
storage, your own GUIDs/versioning, or package signing.

> The exported MOF references the `Microsoft.OSConfig` module by name
> only (no version pin), so packaging succeeds against any installed
> `Microsoft.OSConfig` 1.2.0+. If the module isn't installed,
> The exported MOF uses `ModuleVersion = "0.0.0"` as a portable placeholder.
> Resolve it to the packaging machine's installed version as shown above;
> Machine Configuration runtime requires the exact bundled version. If the module isn't installed,
> `New-GuestConfigurationPackage` fails with *"Failed to find a module
> with the name 'Microsoft.OSConfig'."*
>
> Microsoft.OSConfig 1.3.11 can audit these packages, but its PowerShell
> resource has an upstream `Set()` serialization defect. The example therefore
> defaults to Audit. Use `AuditAndSet` plus an Apply mode only with a newer
> module that contains the fix.

## What changes don't trigger a save

Expand Down
Loading
Loading