mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 04:32:32 +00:00
3946eddcea8713adc01e53cc203ab98ed58efb86
140 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
60a41e9778 |
CI: signing jobs run in the release environment
The code-signing identity trusts only environment:release now, so build-agent release, build-msi sign-loader and release, and publish-scripts publish run in it. Every job that federates to Azure carries an environment; the ref is no longer a trusted subject. Help texts of the storage and endpoint helpers follow: the local build publishes nothing since D-354. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
fd44518e14 |
Bootstrap: installer v2.6 and the OOBE go-script verify the payload catalog (D-354, phase B)
- Install-AutopilotMonitor.ps1 v2.6: Get-AgentPayloadRejections (Test-FileCatalog -Detailed; signature Valid, signer -like the pin, files exactly as catalogued) and Install-VerifiedAgentPayload (extract into Agent-Install, verify there, then move into the agent folder; a rejection deletes the staging folder, so the next run starts clean). Replaces the name-filtered Get-UnverifiedAgentBinaries; the dead "already installed" branch behind guard 5 is gone. - OOBE go-script: embeds the installer's marked block verbatim (EmbeddedResource) and verifies before --install; pin and catalog name come from the Shared constants; the golden is updated, new tests pin the embedding and the order check-before-install. - build-agent.yml: the release verify step dot-sources the installer and runs its own function on the final ZIP. - Pester: every decision branch, a real unsigned catalog, the staging flow (64/64). - Privacy page: the catalog claim; LAST_UPDATED. Merge only after a catalog ZIP is live on stable (plan step V0): pushing this to main publishes the installer to every loader on its next run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
283c094986 |
Agent payload: signed catalog, vendor check, dev channel in CI (D-354, phase A)
- build-agent.yml: input channel (release | dev; dev writes the lab blobs only and refuses stable/release). Every third-party PE needs its vendor's embedded Authenticode signature (signtool verify /pa, read-only). AutopilotMonitor.Agent.cat, a signed SHA-256 catalog over every payload file, is created after Authenticode and before the ZIP; the device check runs under Windows PowerShell 5.1 on the final ZIP. - Agent: SelfUpdater step 5b verifies the staged payload against the catalog before any file is swapped (PayloadCatalogVerifier, Test-FileCatalog in-process; a rejection means no update, skip reason catalog_rejected). agent_started reports which bootstrap installed the agent, read from the bootstrap logs (BootstrapProvenanceReader). - Payload: System.Management.Automation.dll (never loaded, the GAC copy binds first) and AutopilotMonitor.Shared.xml no longer ship; PDBs stay. - Shared: CodeSigningPublisherPattern, AgentPayloadCatalogFileName; CodeSigningParityTests pins them against installer, loader and workflow. - Submodules: infra (build.ps1 local only), internal (D-354, plan, docs), commands. Installer v2.6 and the OOBE script follow once a catalog ZIP is on stable: a merged installer change is live on the next loader run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0f98d9ca17 |
CodeQL: ignore action-pins.yml like ci.yml does
The workflow gates file was missing from both paths-ignore lists, so every gate change re-ran the full CodeQL analysis for nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
587ed74d96 |
deploy-web: Entra redirect URIs from the endpoint registry
Both were repository variables holding the www origin; lib/msalConfig.ts uses them only as the static-render fallback and window.location.origin in the browser. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8d24c5680b |
CI: one endpoint registry for every host the workflows use
.github/scripts/Get-Endpoints.ps1 defines the storage account (blob and table derived from its name), the containers, the AdminConfiguration row, the legacy mirror, the download alias, API, MCP, portal, www, docs and the timestamp server. Workflows load it once per job; the script publisher and the local release build call it; the dev-render anchors are built from the alias entry. deploy-web takes NEXT_PUBLIC_API_BASE_URL and NEXT_PUBLIC_PLATFORM_STATS_MANIFEST_URL from the registry instead of repository variables and refuses an empty value. A new Workflow gates job fails on any host spelled out in a workflow or CI helper; the enforced list is derived from the registry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c26542776e |
CI: credential-free build jobs, trust jobs that never build
Every job that runs third-party code (restore, build, tests, npm ci, next build, wix) now holds only a read-only token and hands its output on as one archive. Signing, publishing and deploying happen in separate jobs on a fresh runner that run only this repository's scripts and pinned actions, unpack artifacts outside the checkout and take no build-job output into a script. - build-agent: reserve -> build -> release - build-msi: sign-loader -> build -> release - deploy-backend: reserve -> build -> deploy (publish output as one tar.gz) - deploy-web: build -> deploy (SWA token only, no id-token) -> record - combine-rules: validate (read-only, PRs too) -> publish (push to main only) - deploy-mcp: same shape; documents docker build as its boundary Deploy-only jobs run in the production environment. The jobs that sign stay without an environment for now: a job carries one OIDC subject, and the signing identity still trusts the ref. oidc-login-test.yml is removed; combine-rules no longer triggers on the dev branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e40471e61a |
Agent release: run all agent test suites before the release build
build-agent.yml tested only DecisionCore (648 of 4,154 agent tests). It now runs the DecisionCore, Agent.V2.Core and SummaryDialog suites before building the release binaries, with the reserved build number. The order matters: Agent.V2.Core.Tests references the agent exe, so its build compiles the agent. Run after the release build or without BUILD_NUMBER, it rewrites the exe with another version (a fresh checkout stamped 2.0.1 in a local simulation). Run first with the number, it stamps the release version and the Build Agent step stays an up-to-date no-op (same FileVersion, same write time). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
af2f2c7abe |
CI: duplicate-code check over src/ with a baseline that only shrinks
tools/quality runs jscpd (5.3.0, pinned with a lockfile like rules/scripts) over the C# and TypeScript sources, tests and generated files excluded, at 100 tokens. The 95 existing clones are frozen in jscpd-baseline.json. A clone that is not in the baseline fails, and so does a baseline entry whose clone is gone: jscpd only reports missing fingerprints, and a stale one would let the same copy come back unnoticed. jscpd scans an LF-normalized copy of the tracked and new files under src/, because with CRLF in a Windows checkout some JSX clones hash differently than on the Linux runner; local runs and CI now see the same 95 clones. At 70 tokens a sixth of all clones were identical using blocks, hence 100. Gate proven both ways with an untracked probe file: a third copy of an existing helper fails with its location, and a baseline entry without its clone fails. New CI job "Duplicate code"; Dependabot watches tools/quality with the usual 7-day cooldown. Locally: npm run dup (check), npm run dup:baseline (shrink). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0709ce2f76 |
Web, MCP: knip gates unused files, exports and dependencies
knip (6.37.0) runs in both TypeScript projects as its own CI step (npm run knip) next to ESLint. It builds the module graph, so "nothing imports this" is checked against every reference instead of a search. Generated files stay out of scope (lib/generated, src/generated); the MCP's Dockerfile-run precompute and its manual scripts are entries, and so is the web's compile-time manifest check. Cleanup to start at zero: - web: four orphaned components (AdminPageSections, SettingsMenu, UserMenu (the navbar renders its own menu) and MarkFailedModal), 17 unused sidebar icons, four unused config constants (the agent-download host allowlist lost its only reader with the /go route in July), the push device-status union that existed twice and was used nowhere, an uncalled push error helper, unused barrel re-exports, default exports duplicating named ones, four superseded DTO interfaces, and a JSDoc type import of an undeclared package. - mcp: two test helpers nothing calls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
69d876b0a6 |
MCP: lint and type-check tests and scripts like the web app
The MCP server had no ESLint, and since its tests left the tsconfig (to keep them out of dist/) nothing type-checked them; scripts/ never was. tsconfig.json now type-checks sources, tests and scripts (noEmit); tsconfig.build.json emits the same dist/ as before (byte-identical) for npm run build and the Docker image. ESLint (the web's versions: eslint 9.39.5, typescript-eslint 8.65.0) runs the recommended rules plus the typed promise rules, and CI's mcp job runs it with --max-warnings 0. Findings fixed: 16 type errors in tests (a fixture missing three wire fields, unknown catch results, untyped mocks); gracefulShutdown was async without awaiting anything, so the signal handlers returned a promise; two dead imports; a Function cast replaced by the exact pipeline() signature; an unused test variable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
224eb668a1 |
Web: helpers live in lib/, generated files in lib/generated/; utils/ is gone
The web app kept helpers in two root folders (lib/ and utils/) and named route-local helper folders either utils/ or lib/. Shared helpers now live in lib/, route-only helpers in a lib/ next to their route (app/dashboard/lib, app/sessions/lib). Generated wire types, manifests, guardrails, error codes and the lastmod file move to lib/generated/, like the MCP server's src/generated/. The session-search parity cases sit next to sessionSearchQuery.ts. Pure move, no behaviour change. Imports, the generators (combine.js, generate-shared-manifest-types.js, sync-error-codes.js, generate-lastmod.js), the CI drift paths, the two backend tests and the MCP freshness test that read these files, and the guard tests' registry path and scan list follow the new locations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
be0e37008a |
Action pins: exact release tag in every version comment (D-331) (#178)
* Action pins: exact release tag in every version comment (D-331) setup-node v7.1.0 and upload-artifact v7.0.2 moved the floating v7 tag, so the "# v7" comments on our still-correct pins (v7.0.0, v7.0.1) stopped naming the pinned commit and zizmor's ref-version-mismatch raised 11 code-scanning alerts on main. The other five major-only comments (checkout, setup-dotnet, cache, attest-build-provenance, functions-action) were one upstream release away from the same alert. All 38 major-only comments now carry the tag their unchanged SHA resolves to (checkout v7.0.1, setup-dotnet v6.0.0, cache v6.1.0, setup-node v7.0.0, upload-artifact v7.0.1, attest-build-provenance v4.2.2, functions-action v1.5.7); no SHA moved, Dependabot keeps advancing SHA and comment together. verify-pins now accepts only a vX.Y.Z comment and names the floating-major case in its own error. Verified: zizmor online (GH_TOKEN) reproduced the 11 findings before and reports 0 ref-version-mismatch after, --min-severity low exits 0; the verify-pins script passes all 51 uses lines and rejects a major-only comment, a missing comment and a tag reference in a scratch workflow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * bump pointer: internal (D-331 action pin comments) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
04fae4fe86 |
Web Push channel: alerts to paired phones without a third-party messenger (#177)
* bump pointer: internal (ingest-session-row-reads RELEASED, measurement) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Web Push channel: alerts to paired phones without a third-party messenger (D-327 to D-330) Provider WebhookProviderType.Push = 50 in both channel scopes. It carries no destination; NotificationChannelDispatcher takes a NotificationScope from every caller and the push transport resolves the scope's paired devices at send time (table-backed Admin/Operator, GlobalAdmin with matching identity binding; Viewer and Global Reader never). The receiver never signs in: a signed-in Admin/Operator creates a one-shot pairing code (11 Crockford chars, hashed, 10 min, ETag one-shot, five-failure burn), the phone redeems it anonymously, the portal session confirms. The phone's only credential is the device token in X-Push-Device-Token, good for status, re-subscribe and unpair of its own row (10 calls per minute per device; the anonymous pairing routes 10 per minute per IP). Payloads are RFC 8291 per device (new BCL-only library src/Push with the RFC vector pinned byte for byte) with a VAPID key ring and the kid on every device row; the projection reads an allow-list only (never Sections, DataJson, Failure Reason, Last Failure or the ops message), serial masked, 2 KB. 404/410 mark a device Stale instead of deleting it and the receiver re-arms it on its next open (the same PUT lifts a delivery-failures pause); devices pause when their owner stops signing in (PushOwners stamp in auth/me) through one pause path and resume by themselves; an owner without a stamp is paused, never deleted outright; role loss revokes with a wipe push and deletes the row (there is no Revoked state). PushDevices and PushPairingGrants are credential-bearing: raw-table deny-list, no backup, redacted dependency rows. Receiver app under /push/ (module service worker, Declarative Web Push shape, local history, pairing and status pages, manifest + icons), portal UI (Push provider in the channel editor, pairing dialog with QR code, device lists for tenant and platform scope, "Notify me when done" on the session page — the watch row records the watcher's own scopes, so a Global Admin watching a customer session is notified on the platform-paired phone), 22 catalogued routes, ops events PushDeliveryFailed and PushEndpointRefused (deduplicated per instance), maintenance sweep inside the existing run, settings template. The tenant-scope provider stays Global-Admin-only until the customer release (same ValidateModel gate as Telegram). Backend 7371, library 125, MCP 997, web 2061 tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * bump pointer: internal, .claude/commands, infra (push-relay: plan, D-327 to D-330, markers, VAPID generator) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * bump pointer: internal (push-relay: commit step ticked) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Drop sprintf-js from both lockfiles (Dependabot #114, #117) GHSA-hp3w-g68c-fv3c (sprintf-js <= 1.1.3, denial of service through unbounded precision specifiers) has no patched release, so both chains are cut instead. MCP (#117): sprintf-js arrived via @huggingface/transformers -> onnxruntime-node -> global-agent 3 -> roarr. global-agent is only loaded by onnxruntime-node's CUDA install script, which the image never runs (npm ci --ignore-scripts). The override global-agent ^4.1.3 drops roarr; 4.x keeps the bootstrap export that script calls, and onnxruntime-node 1.30 depends on the same range. Rules tooling (#114): ajv-cli 5.0.0 (no release since 2023) pulled js-yaml 3 -> argparse 1 -> sprintf-js. rules/scripts/validate.js now checks the four rule sets against their schemas with ajv (draft 2020-12) and ajv-formats directly, and combine-rules.yml runs it in one step instead of four ajv-cli loops. The lockfile shrinks from 26 to 6 packages; the fast-json-patch override and the legacy-peer-deps .npmrc (only there for ajv-cli's ts-node peer) are gone. Verified: validate.js accepts all 149 rule files plus guardrails.json exactly as ajv-cli did and rejects a corrupted copy (missing field, broken JSON, U+2028 in a guardrail value) with exit 1; clean npm ci + combine.js leave the generated outputs unchanged; MCP build and 997 tests green; zizmor clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * bump pointer: internal, .claude/commands (validate.js replaces ajv-cli: B-gzv, rule-builder fallback) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Bump sharp, proxy-addr and source-map-js past fresh advisories (MCP, Web) - sharp 0.35.4 -> 0.35.5 (GHSA-wq5f-xc86-pv6w, librsvg): the existing sharp override in both projects now floors at ^0.35.5 so the fixed build cannot be undercut by a parent range. - proxy-addr 2.0.7 -> 2.0.8 (GHSA-jqcg-44mw-7w3h, via express; MCP only) and source-map-js 1.2.1 -> 1.2.2 (GHSA-68fv-2mgg-jv7q, via postcss) are lockfile-only bumps inside the ranges their parents declare. No other lockfile entry moved. npm audit in the MCP server is clean. Verified: MCP build and 997 tests green; Web tsc clean, 2061 tests green, next build (static export) succeeds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * bump pointer: internal (push-relay: CORS verified, cert-exclusion prerequisite, EU app facts) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Web: Next.js 16.3.6 -> 16.3.8 (six advisories) 16.3.8 closes GHSA-3w37-wq28-93x7, GHSA-4jqv-mc3x-m676, GHSA-39w2-rjm5-chcv, GHSA-f87g-xv8r-7p7x, GHSA-mcj8-r9mp-w47p and GHSA-cjq9-62q9-8jv4 (affected 16.0.0 - 16.3.7). Under output:"export" the cache, ISR and image-optimization paths never run on the SWA; the dev-server disclosure is local-only. The patch is taken anyway. next, eslint-config-next and @next/bundle-analyzer stay aligned and floor at ^16.3.8; the lockfile is pinned to 16.3.8, not the 16.4.0 minor the caret would resolve to. What npm audit still reports (braces, micromatch, chokidar, fast-glob, postcss-selector-parser) comes through Tailwind 3 and eslint-config-next, build-time only; braces has no fix, the rest needs Tailwind 4 (backlog). Verified: tsc clean, eslint 0 findings, 2061 tests green, next build (16.3.8, 102 static pages) succeeds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * bump pointer: internal (B-dzs lockfile merge note, B-hxz braces chain) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
9f46adcdfa |
ci: the storage auth helper masks its token in the run log, like the Key Vault token
GitHub masks only secrets.* by itself. The bearer token Get-StorageAuthHeaders.ps1 mints at run time was never registered as a mask, unlike the Key Vault signing token, which build-agent, build-msi and publish-scripts mask before exporting it. No caller prints it today -- every call site captures the hashtable and passes it only as -Headers -- but this repository's run logs are public, and a single debugging line would have published it: an uncaptured call of the helper, or Get-Error / Format-List on a failed request, whose TargetObject is the HttpRequestMessage including the Authorization header (reproduced locally with a dummy token). The helper now emits ::add-mask:: for every token it hands out, which covers all six workflows in one place. Write-Host, never Write-Output: the output stream is the script's return value. Verified with a mocked az that the result stays one hashtable and that a nested caller in the Request-BuildNumber.ps1 pattern still returns only its build number. Only under GITHUB_ACTIONS -- locally the line would print the token instead of hiding it. Prompted by an external AI review of the public scripts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
762fef4e24 |
Landing page bakes the platform stats of the deploy into its static HTML; browsers then swap in the live numbers
Crawlers and LLM fetchers run no JS and saw only the skeleton of the stats band. app/page.tsx now awaits one read of the public stats blob during next build (lib/platformStats.ts: manifest, then the versioned file it names) and renders the numbers into the export. StatsBand starts from that snapshot and replaces it with the live numbers; a failed live read keeps the baked numbers instead of hiding the band. The read is awaited at page level, not under Suspense: React moves a resolved boundary of a page this size into a hidden div at the end of the body. It uses no-store (route force-static), so Next's fetch cache cannot hand a later build the previous numbers. Fail-soft: without a snapshot the page ships the skeleton and loads live as before, and deploy-web.yml warns. The browser now revalidates both files (no-cache instead of force-cache): the backend rewrites the per-day file every 2 hours under immutable, so a copy cached earlier that day would have made the numbers drop after load. The page body moved to components/landing/LandingPage.tsx, which the marketing guards render synchronously; StatsBand left the .json() ratchet baseline; the sitemap lastmod of / includes the body. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4ddfosgdeCwXo7NvB9xd2 |
||
|
|
090f47d4fb | Merge branch 'main' of https://github.com/okieselbach/AutopilotMonitor | ||
|
|
5b0dfaad99 |
Landing shows the GitHub star count and a customer quote
The navbar gets a "Star" pill with the repository's star count from sm up; phones keep the bare GitHub mark. A new section before the closing CTA shows one customer quote with a star ask underneath. The footer's "Star us on GitHub" line is gone, so the page asks twice, not three times. The count is read once per web deploy (deploy-web.yml, own step, job token only there) and inlined by next build as NEXT_PUBLIC_GITHUB_STARS, so visitors' browsers never contact GitHub and the CSP stays as it is. Without a value the pill renders without a counter (D-303). The desktop links, bell and sign-in now start at lg instead of md: the bar already overflowed between 768 and ~1000 px and the pill widened it further. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
f0aec031f3 |
chore(deps): bump the actions group with 2 updates (#168)
Bumps the actions group with 2 updates: [github/codeql-action/init](https://github.com/github/codeql-action) and [github/codeql-action/analyze](https://github.com/github/codeql-action). Updates `github/codeql-action/init` from 4.38.0 to 4.38.2 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/b96794f015dfd88f77b49b1c93e0fa7110f94c63...2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2) Updates `github/codeql-action/analyze` from 4.38.0 to 4.38.2 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/b96794f015dfd88f77b49b1c93e0fa7110f94c63...2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2) --- updated-dependencies: - dependency-name: github/codeql-action/init dependency-version: 4.38.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: github/codeql-action/analyze dependency-version: 4.38.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
b6791d37e0 |
ci(codeql): exclude js/missing-rate-limiting; a test pins every OAuth route to its limiter (D-281)
The query recognizes only npm limiter packages, never oauthRateLimit / oauthTokenRateLimit, and flags a handler as soon as it calls a function named like *auth*, *login* or *verify*. All six alerts it ever raised were on throttled OAuth routes (the last two came from tenantAuthority()). What it stood in for is now a test: every route of the OAuth router must run a per-IP limiter first unless it is one of the three named discovery documents, /oauth/token must carry the strict budget, and a router.use() middleware fails the check. Verified by mutation: removing the limiter from /oauth/authorize fails the test with the route name. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
fb8703a4c6 |
chore(deps): bump the actions group with 3 updates (#160)
Bumps the actions group with 3 updates: [azure/login](https://github.com/azure/login), [github/codeql-action/init](https://github.com/github/codeql-action) and [github/codeql-action/analyze](https://github.com/github/codeql-action). Updates `azure/login` from 3.0.2 to 3.1.0 - [Release notes](https://github.com/azure/login/releases) - [Commits](https://github.com/azure/login/compare/7ddb5af1ef8758cf1353cf3b42f940aee27ba21c...a641126d1b8aa4d1fa005f4f92df94a3a4c4c906) Updates `github/codeql-action/init` from 4.37.9 to 4.38.0 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/cdf488f595d80d6e07e03d4674febd5ab45fa938...b96794f015dfd88f77b49b1c93e0fa7110f94c63) Updates `github/codeql-action/analyze` from 4.37.9 to 4.38.0 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/cdf488f595d80d6e07e03d4674febd5ab45fa938...b96794f015dfd88f77b49b1c93e0fa7110f94c63) --- updated-dependencies: - dependency-name: azure/login dependency-version: 3.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions - dependency-name: github/codeql-action/init dependency-version: 4.38.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions - dependency-name: github/codeql-action/analyze dependency-version: 4.38.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
1b27babd17 |
ci: zizmor gate for the workflow files plus a clean baseline (D-268)
zizmor (static analysis for GitHub Actions) now runs twice: locally before a
workflow commit (hard rule in .claude/CLAUDE.md) and as the `zizmor` job next to
the pin gate in action-pins.yml (zizmorcore/zizmor-action pinned by SHA, zizmor
1.30.1 pinned, SARIF into Code scanning, severity low and above). Like the pin
gate it reports and does not block. zizmor.yml holds the single reasoned ignore.
Baseline fixes so the gate starts green:
- persist-credentials: false on every checkout except combine-rules.yml, the one
job that pushes with the checkout credentials (ignored in zizmor.yml, with reason)
- publish-scripts.yml: id-token: write moves from the workflow to the publish job;
indexnow-notify.yml: permissions: {}
- deploy-mcp.yml: env values referenced as shell variables inside bash steps
instead of template expansion
- pester.yml / publish-scripts.yml: reusable workflow via the self-repository form
$/ (GitHub changelog 2026-07-30); the pin gate treats $/ like ./
- combine-rules.yml: ajv-cli installed with npm ci from rules/scripts/package-lock.json
(no ts-node peer, no lifecycle scripts) instead of an ad-hoc global install;
Dependabot watches /rules/scripts
- azure/login pin comment corrected to the tag the SHA actually is (v3.0.2; v3
moved on to v3.1.0), no runtime change
- Dependabot: 7-day cooldown on every ecosystem (security updates are exempt)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
||
|
|
f5efadfd07 |
ci: pin every third-party action to a commit SHA, gated and Dependabot-maintained (D-226)
A tag is a mutable pointer owned by the action's publisher. These jobs are not ordinary CI: they hold the code-signing Key Vault token (D-223), the federated storage credentials (D-222), and combine-rules.yml holds a contents:write token that pushes to main. Whoever could repoint v7 would run code with all of that. azure/login and Azure/functions-action were already pinned; the actions/* and github/codeql-action references were not, on the usual argument that GitHub owns them and shares the runner's trust boundary. That argument does not survive the fact that they run in the same jobs as the signing identity. All seven are now pinned by SHA with the version in a trailing comment -- the comment is what keeps the line reviewable, and Dependabot rewrites both together. action-pins.yml is the guard: pins nobody enforces rot, and no review reliably catches one @v4 added to a 200-line workflow. It lives in its own workflow because ci.yml deliberately ignores changes to the other workflow files, so a job placed there would stay silent for exactly the commits it must inspect. Like CI it reports rather than blocks -- main is unprotected; making it binding would mean adding it as a required status check, which is a branch-protection decision. Verified both directions: 49 references pass against the repo, and a fixture with @v7, @main and a bare SHA fails with three annotations while the local reusable workflow refs are skipped. Two floating tool versions closed while here. wix went from the range 5.* to 5.0.2 -- it writes the bytes of the MSI customers install, where AzureSignTool only signs them. ajv was the worse one: unpinned, and npm runs lifecycle scripts at install, so a hijacked release would have executed with the push token before a single rule was validated; now exact versions with --ignore-scripts. Line endings: sed -i rewrote the workflow files as LF. Git normalises on commit, so the content diff is unaffected and indexnow-notify.yml carries no change at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c24c7bf7ef |
fix(publish): serve the customer scripts as a download, not as a web page
The blobs carry Content-Type: text/plain and no Content-Disposition, so every browser renders them inline -- the documented "download the bootstrapper script" step turns into "select this text and save it yourself", with whatever the editor then does to the line endings and the signature block. x-ms-blob-content-disposition makes the link a download in every browser, which is a server-side fact rather than a per-browser hint in the docs. Command-line clients (the loader, the MSI runner, curl) ignore the header, so nothing on the device path changes. Metadata only: the script bytes are untouched, so no version bump and the bump guard still sees the published copies as current. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
647581b4ff |
refactor(msi): the MSI carries the shared stage-1 loader instead of its own downloader
Invoke-BootstrapDownload.ps1 and Start-AutopilotMonitor.ps1 had become the same script: download the current installer, run it, log, always exit 0. The MSI copy additionally downloaded and executed WITHOUT verifying what it got, which is exactly the gap the loader was written to close. The MSI is now packaging only -- it installs and runs the same file that is assigned directly in Intune, and both delivery channels get publisher verification from one implementation. Dropped from the MSI path: its own Deployed-marker check. That guard lives in stage 2 and runs there anyway, so the only cost is one download on a device that is already deployed. Kept: the log file name. bootstrap-msi.log is what the diagnostics tooling keys on to tell the MDM-LOB channel apart from the Intune script channel, so the loader takes -LogFileName (default bootstrap-loader.log) and the custom action passes bootstrap-msi.log. A Pester test now pins that contract across the two files -- renaming the parameter on either side would leave the MSI channel logging under the wrong name. Package version 1.0.2 -> 1.1.0 so Intune LOB update detection sees the new payload, and a new component GUID because the key path file changed. Loader 1.0 -> 1.1. Bootstrap suite on Windows PowerShell 5.1: 44/44. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7563435837 |
feat(ci): Authenticode-sign the bootstrap MSI and the runner it embeds (D-223)
MSI carries an Authenticode signature like any PE, so the same identity and the same ordering rules apply: signing identity logs in first and only for the Key Vault token, the storage login stays last, and no environment: on the job. The embedded Invoke-BootstrapDownload.ps1 is signed BEFORE wix build -- wix embeds the file as it finds it, so signing afterwards would leave the copy inside the MSI unsigned, and a device under an AllSigned policy needs the runner to carry its own signature. The MSI is signed after the build and before the hash and the upload, then both files are verified (Valid, timestamped, optional publisher pin) while nothing is published yet. The provenance attestation now also skips runs that upload nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
47dad7143f |
docs(ci): tighten the code-signing comments in build-agent
Public repo: keep the reasoning that a later change could break -- signing must precede the release hashes, the job must never carry an environment, the timestamp is not optional -- and drop the rest. Comments only, no behaviour change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
cc6be10532 |
fix(publish): read the signed bytes back as byte[], not as a wrapped Object[]
The ',' operator elsewhere in this file exists because PowerShell unrolls an array RETURNED from a function; a property assignment does not unroll, so it wrapped the byte[] in an Object[] of length 1 and the upload failed converting it back. Signing itself was fine -- run 34253331703 signed all six scripts and uploaded none, so nothing reached the download alias. Added the invariant that would have caught it: signed bytes must be longer than the unsigned form, since a signature only ever adds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0ac548bfce |
feat(bootstrap): two-stage loader and Authenticode-signed customer scripts (D-224)
Start-AutopilotMonitor.ps1 is the new stage 1 -- the file an administrator assigns in Intune. It carries no enrollment logic: it downloads the published installer, verifies its Authenticode signature, and runs it in the same process, passing through every remaining argument. Assigning the installer directly stays supported and unchanged. The point of the split is that the customer-held artifact stops changing. Today every bootstrap change forces customers to replace their Intune copy, so the fleet runs a distribution of script versions we do not control. It is also what makes script signing worth doing: signing a file that gets swapped by hand each release is a one-off gain, signing a loader that never changes is permanent. The loader is deliberately minimal, because it is the one piece that cannot be fixed remotely -- every guard placed in it would be frozen for years. All guards stay in stage 2. Verification pins the certificate SUBJECT, never a thumbprint: certificates get renewed and a deployed loader cannot be, so a thumbprint pin would strand the fleet at the next renewal. A hash oracle was rejected for a different reason -- it would have to be fetched from the same host as the script, so it could not survive a compromise of that host, while the expected publisher travels inside the loader. Fail-closed on verification, fail-soft on exit: nothing runs without a valid signature from us (running unverified code as SYSTEM is the risk being removed here), but the loader still exits 0, because a device without monitoring is acceptable and a failed enrollment step is not. The accepted cost is that a device which can never fetch or verify stays silent. Publisher changes: - every published script is signed (loader, installer, both -Dev renders, Test-ShouldBootstrapAgent, Grant-AutopilotMonitorAddOn), and signing is the LAST transformation: it covers exactly the CRLF/UTF-8 bytes Get-PublishBytes produced, so nothing may normalise or re-encode them afterwards. - signing is mandatory for a real publish. Only -DryRun may run unsigned, because the PR gate executes the publisher without any Azure login. - comparisons against a published copy strip the signature block first. A timestamped signature differs on every run, so without stripping the version-bump guard and the drift check would report a change for every unchanged script. - the bump guard became Assert-VersionBump and now covers the loader as well. - version.json gains loaderVersion. Additive, no consumer yet: stage 2 is current by construction, so only stage 1's version says anything about a customer's Intune copy. - -SignedOutputDir writes the exact uploaded bytes for the CI artifact, in a dry run too -- that is how a signed script gets inspected before it reaches anyone. publish-scripts.yml logs in with the signing identity first and only to fetch the Key Vault token (azure/login sets the default az account and the storage login has to be the one that stays), then publishes and offers the signed scripts as an artifact. build-agent.yml passes the same parameters on a stable cutover. Tests: 14 new Pester cases, including that a validly signed script from a FOREIGN publisher is refused -- that is what an attacker with any code-signing certificate can produce. Full bootstrap suite on Windows PowerShell 5.1: 43/43. Not covered: the MSI runner still downloads stage 2 without verifying it, and the customer docs still describe the direct assignment only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9c1d0586ef |
ci: skip the provenance attestation on test runs of build-agent
A dispatch with upload_blob=false and create_release=false publishes nothing, so the Sigstore attestation would write a permanent public transparency-log entry for bytes nobody ever receives. The step now runs only when the run actually ships something. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
21ec5e4773 |
ci: Authenticode-sign the agent binaries before the release hashes are computed (D-223)
Every assembly we author -- AutopilotMonitor.Agent.exe, AutopilotMonitor.SummaryDialog.exe and the three AutopilotMonitor.*.dll beside them -- is signed with AzureSignTool (pinned 7.0.1) against a certificate in the corporate code-signing Key Vault, SHA-256 file digest plus an RFC-3161 timestamp. Third-party assemblies keep their own publisher's signature. The position of the step is load-bearing: signing rewrites the PE file, so it has to run before the staging copy, the ZIP and every SHA-256 in "Create release artifact". Those hashes are the bootstrap's integrity oracle (version.json, AdminConfiguration) -- signing after hashing would publish hashes that no downloaded file can ever match. The signing identity is a SECOND federated principal in the same tenant and logs in FIRST, only to fetch a Key Vault token; the storage login of D-222 follows and owns the az context from there, because azure/login sets the default account and the last login wins. Both federated credentials carry the subject repo:okieselbach/AutopilotMonitor:ref:refs/heads/main, so this job must never be given a GitHub 'environment:' -- that rewrites the OIDC subject claim and would break BOTH logins at once. No unsigned fallback and no dispatch input to skip signing: a missing repository variable, a Key Vault failure or a non-zero azuresigntool exit code fails the run before anything is published. azuresigntool is a native tool, so $LASTEXITCODE is checked explicitly -- PowerShell does not throw on a non-zero exit, and a silently unsigned release is exactly the outcome signing exists to prevent. Verification runs against the STAGED bytes, not the build output: every AutopilotMonitor.* PE in the ZIP must be Valid, must carry a timestamp (without one the signature dies with the certificate) and all of them must share one signing certificate. Setting the optional repository variable CODE_SIGN_EXPECTED_SUBJECT additionally pins the publisher, so the gate proves "signed by us" instead of "signed by someone trusted". The build provenance attestation now covers the ZIP and both EXEs, so an extracted binary can be verified without the ZIP. Authenticode and the attestation answer different questions and both stay: who published this binary (offline, what Windows, SmartScreen and WDAC evaluate) versus which build produced these exact bytes (gh attestation verify). Not covered here: the customer bootstrap script (its signature block changes the published bytes and must follow the dev render; see B-57c, the two-stage loader), the bootstrap MSI, and the local build.ps1 path, which from now on produces unsigned bits. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e4a5a6654d |
ci: workflows authorize Azure Storage with the federated identity, not SAS secrets (D-222)
Every storage write against autopilotmonitoreu -- build-counter CAS, agent ZIP and MSI, version manifests, customer bootstrap scripts, the AdminConfiguration hash oracle -- now rides on the existing federated credential (subject repo:.../ref:refs/heads/main) instead of a long-lived SAS in repo secrets. The REST calls themselves are unchanged: '?<sas>' became an Authorization header, so the ETag-CAS of the counters, Content-MD5 and the no-cache contract survive verbatim. That is also why this is not a rewrite onto 'az storage blob upload' -- it cannot express If-Match, and two concurrent builds could then mint the same build number. .github/scripts/Get-StorageAuthHeaders.ps1 is the single place that knows the resource id, the az invocation and the service version. x-ms-version is mandatory for Entra-authorized requests (>= 2017-11-09) and for any Table request without a SAS; 2021-08-06 was verified live against both the blob and the table endpoint of this account. azure/login added to build-agent, build-msi, publish-scripts (plus id-token: write) and deploy-web; deploy-backend and deploy-mcp already had it. Publish-BootstrapScripts.ps1 -DryRun acquires NO token: the PR gate runs it without any Azure login, and that must stay true. The AdminConfiguration merge lost its "secret not configured" skip branch -- a failure there now surfaces instead of silently leaving the hash oracle pointing at an older build. AZURE_BLOB_AGENT_SAS_TOKEN, AZURE_BLOB_VERSIONS_SAS_TOKEN and AZURE_TABLE_ADMIN_SAS_TOKEN are retired once the first runs are green. AZURE_BLOB_AGENT_LEGACY_SAS_TOKEN stays: that account lives in the pre-migration tenant, where this principal has no identity -- it dies with the mirror, not before. Requires on the SP: Storage Blob Data Contributor on the containers 'agent' and 'versions', Storage Table Data Contributor on the table 'AdminConfiguration'. Granted 2026-09-08. |
||
|
|
4e73ba8900 | Agent Host changes for agents/improve-changelog-promotion-ui (#152) | ||
|
|
38b667799c |
ci: workflow-only commits no longer trigger CI and CodeQL
The
|
||
|
|
a5f74d6abc |
ci(agent): release body no longer repeats the release title
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
d0e09301a1 |
ci(agent): release body drops the Installation and Included Components sections
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
b89008e745 |
ci(agent): customer release notes come in through the release_notes dispatch input, never from CI (D-187)
build-agent.yml gains an optional release_notes input; the release body opens with "What's new" from that input, or a generic fallback linking to the docs changelog plus a marker the operator tooling uses to anchor the next notes, so releases dispatched by hand leave no gap. --generate-notes dropped (pull requests only); a compare link to the previous tag replaces it. Plan-first rule: plans live in internal/work/plans from the first draft. Gitlinks: internal, .claude/commands. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
9eb8cac507 |
ci: skip CI and CodeQL on gitlink bumps and prose-only commits
Every "chore: bump internal gitlink" commit ran the full CI matrix and both CodeQL analyses, because a gitlink bump reports the bare path 'internal' (no trailing slash) and the existing 'internal/**' filter does not match it. Both workflows now ignore the bare and nested forms of the three private submodule paths plus markdown, LICENSE and the .github metadata files. CodeQL's weekly unfiltered schedule remains the backstop. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
db020789e1 |
chore: private submodule docs/ becomes internal/ (knowledge + decisions + work files + private instructions)
- .gitmodules: docs -> internal, repo renamed to Autopilot-Monitor-internal; the OKF bundle now lives at internal/docs/ - .claude/CLAUDE.md rewritten: where knowledge lives, working-file rules, hard rules; imports the private half from internal/CLAUDE.md (silently absent without submodule access) - .claude/architecture-principles.md moved into the internal bundle (reconciled against the code) - tasks/ (gitignored) retired; working files live in internal/work/ - doc path references docs/<bundle> -> internal/docs/<bundle> in code comments, skills gitlink, codeql paths, build script - rule descriptions no longer cite an internal doc path (customer-visible text); rules/dist regenerated - public tree no longer names private operator tooling - .claude/commands gitlink: skill docs point at the new paths |
||
|
|
a4140c4e4c |
docs(readme): refresh screenshots, add social preview source
The four README screenshots were still from the private preview (old blue logo, preview banner). Retaken in the light theme with the current logo, tenant view only, no banner. Fleet Health uses the 90-day window; the geo map has too few locations in the own tenant to read as a world map. Also stores the GitHub social preview as source (HTML composition plus the anonymized dashboard capture it embeds) and the rendered 1280x640 PNG. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
ce9efb6f6c |
docs(repo): AGPL-3.0 root license, MCP section in README, security policy and issue forms
- Root LICENSE is now the verbatim AGPL-3.0 text so GitHub detects the license instead of showing "Other"; the split model (MIT for Agent + Shared) is explained in the README and each component keeps its own license file. - README: MCP server in the component list, an "AI Integration (MCP)" section with example prompts and the server URL, and a neutral Feedback section. - SECURITY.md routes reports to private GitHub Security Advisories. - CONTRIBUTING.md is a feedback routing page (issues, discussions, security, Report Session button), CODE_OF_CONDUCT.md is short and neutral. - Issue forms for bug reports and feature requests; blank issues disabled. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
9207b066db | changed dependabot to batching | ||
|
|
4c32d66a63 |
ci: print failed-test messages in the .NET test steps
`dotnet test --verbosity quiet` also sets the console logger to quiet, which drops the "Error Message" block of a failed test — run 33581374985 showed only four "[FAIL]" lines for the V2 Core scenario flake and nothing about which fixture budget blew. An explicit `--logger "console;verbosity=minimal"` keeps the build quiet but prints the failed tests with message and stack. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
19c4354224 |
docs: README assets move to .github/assets, drop the architecture link
The screenshots and the logo lived in docs/images, which is now a private submodule and cannot render on the public README. They are README assets only (no doc page references them), so they move to .github/assets — the usual home for repository-level images — and leave the docs bundle. The architecture link pointed into the private docs; the technical documentation is not part of the public surface any more. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d763a885e2 |
Ops alert channels, ops-events server-side filters, and generated MCP tool vocabularies (#140)
* feat(backend,web): Telegram als Channel-Provider + ein einziger Dispatch-Pfad Notifications hatten zwei Sendewege: WebhookNotificationService.SendToChannelsAsync fuer tenant-konfigurierte Channels, und TelegramNotificationService direkt aus dem Ops-Alert-Pfad. Ein neuer Provider haette beide anfassen muessen. Neu: NotificationChannelDispatcher ist DIE Channel-Sende-API und routet pro Provider — WebhookProviderType.Telegram zum Bot, alles andere als gerenderten Webhook-POST. SendToChannelsAsync wurde aus WebhookNotificationService entfernt statt dupliziert, damit es keinen zweiten Pfad gibt; alle sieben Call-Sites ziehen um (EventIngestProcessor, RegisterSession, ReportDistress, SlaBreachEvaluation, AnalyzeOnEnrollmentEnd, TestWebhookNotification). SendOpsAlertAsync/SendNotificationAsync sind virtual (Test-Seam), TelegramNotificationService bekommt SendAlertWithResultAsync fuer die Test-Endpoints und RenderAlertText als testbaren Kern. Telegram (=40) ist der Sonderfall: kein Webhook. Der Channel-Url traegt die CHAT-ID, der Bot-Token gehoert der Plattform (PreviewConfig). Ein Tenant-Admin wuerde durch UNSEREN Bot senden — deshalb ist das Gate eine Server-Regel, keine versteckte Dropdown-Option: ValidateTelegramChannelGate sitzt in TenantConfigValidation.ValidateModel und deckt damit BEIDE Schreibpfade ab (Full-Model-PUT und den MCP-Field-Patch ueber TenantConfigPatchService) — in der Update-Function allein waere der Patch-Pfad daran vorbeigelaufen. Ein unveraenderter, GA-angelegter Channel passiert das Gate, damit ein Tenant-Admin unrelated Config weiter speichern kann. Telegram-Ziele ueberspringen SsrfGuard.ValidateWebhookUrlFormat (eine Chat-ID ist keine URL und wuerde immer failen) und werden stattdessen als numerische Id oder @username geprueft. Mitgeliefert, weil der Dispatcher und die Ops-Seite sie brauchen: NotificationChannel.RedactList / RestoreRedactedList als geteilte Helfer hochgezogen (TenantConfiguration delegiert jetzt dorthin, statt eine zweite Kopie zu pflegen), AdminConfiguration.OpsNotificationChannelsJson + GetOpsNotificationChannels() mit Legacy-Slot-Synthese, OpsAlertRule.NotifyChannelIds/IncludePayload, NotificationAlert.DataJson (nur GenericJsonRenderer serialisiert es, additiv in schemaVersion 1.0). Web: der Channel-Editor liegt jetzt in components/notifications/ChannelEditor.tsx und wird von Tenant- und Admin-Seite geteilt (showEventToggles trennt die Faelle). Telegram erscheint im Provider-Dropdown nur fuer Global Admins (Prop aus useAuth().user?.isGlobalAdmin, Hausmuster aus SectionAutopilotValidation); ein bereits konfigurierter Telegram-Channel bleibt fuer alle sichtbar, damit ein Speichern ihn nicht still verwirft. Tests: TelegramChannelProviderTests (27) — Dispatcher-Routing inkl. "Telegram erreicht NIE den SSRF-Pfad", Chat-ID-Validierung, GA-Gate auf beiden Schreibpfaden. Mutationsgeprueft: Gate invertiert => 5 rot, zurueck => 27 gruen. OpsNotificationChannelsTests + OpsAlertPayloadTests decken die mitgelieferten Modell-Additionen ab. TestNotifications ist der gemeinsame inerte Stub, der die 17 bestehenden OpsAlertDispatchService-Konstruktionen ersetzt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(backend,web): Ops-Alerts routen auf benannte Channels statt auf drei feste Slots OpsAlertRule hatte kein Ziel. Jede matchende Regel ging an alle drei fest verdrahteten Slots in AdminConfiguration (Telegram-ChatId / Teams-URL / Slack-URL), und nur die ERSTE matchende Regel wurde ueberhaupt ausgewertet (FirstOrDefault). Ein Conversion-Signal an ein Sales-Team zu binden hiess also: das Sales-Team bekommt auch jede Security-Regel. Jetzt: AdminConfiguration.OpsNotificationChannelsJson haelt eine benannte Channel-Liste (dasselbe NotificationChannel-Modell wie tenant-seitig, nur anderer Scope), und OpsAlertRule.NotifyChannelIds bindet Regeln daran. Dispatch wertet ALLE matchenden Regeln aus (Where statt FirstOrDefault) und vereinigt ihre Ziele. Rueckwaertskompatibel ohne Migrationsjob: - Leere Channel-Liste => GetOpsNotificationChannels() synthetisiert je konfiguriertem Legacy-Slot einen Channel mit stabiler Id (legacy-telegram heisst "Autopilot Monitor IT Engineer") und uebernimmt die Legacy-Enabled-Flags — ein bewusst abgeschalteter Provider bleibt abgeschaltet. - GetAdminConfigurationFunction projiziert DIESELBE Synthese in das Read-Model, damit der Editor zeigt, was heute tatsaechlich Alerts empfaengt, und der erste Save exakt das materialisiert. Die Alternative waere eine zweite Kopie der Synthese-Regeln im Web gewesen, die vom Dispatcher wegdriftet. - Leere NotifyChannelIds = alle aktivierten Channels. Das ist, was jede heute existierende Regel traegt, also verhaelt sich am Deploy-Tag nichts anders. - Ids, die nicht mehr aufloesen, werden verworfen statt auf Broadcast zurueckzufallen: eine Regel, deren einziger Channel geloescht wurde, muss NICHTS erreichen, nicht alles. Die Legacy-Felder bleiben als Migrationsquelle stehen und werden von der Alerts-Section nicht mehr geschrieben — aufraeumen erst, wenn produktiv Channels stehen. Sicherheit: OpsNotificationChannelsJson ist secret-tragend und steht in der Deny-List von RedactedCopyForReader() (ConfigRedactionTests bewacht Drift). Der Admin-PUT hat bisher keinen Redacted-Restore — er bekommt jetzt einen fuer die Channel-Liste, damit ein zurueckgespieltes Reader-View nicht "***REDACTED***" als Chat-ID persistiert, plus die strukturelle Validierung, die auch die Tenant-Liste passiert. Neuer Endpoint POST /api/global/config/test-ops-channel (GlobalAdminOnly, im Policy-Catalog registriert). Kein neues Response-DTO: TestWebhookNotificationResponse hat identische Semantik und wird geteilt — nur der Doc-Kommentar nennt jetzt beide Routen. Web: die "Alert Providers"-Karte weicht der Channel-Liste (geteilter ChannelEditor, im Admin-Kontext mit Telegram und ohne die tenant-spezifischen Event-Toggles). Jede Regel bekommt einen Channel-Picker; leere Auswahl rendert als "all channels", nicht als Warnung — hier ist das der gueltige Default. Die Bindungslogik liegt testbar in opsChannelRouting.ts und spiegelt ResolveTargets: der erste explizite Pick startet vom LEEREN Set, sonst liesse sich eine Broadcast-Regel nie verengen. handleSaveOpsAlertConfig nimmt jetzt ein Objekt statt elf positionaler Argumente — bei einem Dutzend Feldern schreibt ein vertauschtes Paar sonst still eine Webhook-URL in den falschen Provider-Slot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(backend): TenantTrialStarted + Event-Payload optional pro Regel Der Trial-Start war stumm. POST config/{tenantId}/trial schrieb nur einen Audit-Entry, und der GA-Pfad (PATCH plan) feuerte nur beim Downgrade — also war genau der Conversion-Moment, den Sales/Support interessiert, das einzige Plan-Ereignis ohne Ops-Event. RecordTenantTrialStartedAsync (Tenant/Info) feuert jetzt aus BEIDEN Schreibpfaden, unterschieden per selfService-Flag, und jeweils NACH dem persistierten Save (SaveConfigurationAsync ist fail-loud), damit kein Alert eine Trial ankuendigt, die nicht gespeichert wurde. Der GA-Pfad haengt an der aufgezeichneten Aenderung, nicht am Request: ein reiner planTier-Edit bleibt still, ein idempotenter Save mit demselben Datum ebenfalls, und das Beenden einer Trial (explizit null) ist ein Downgrade und bleibt bei TenantPlanDowngraded. Payload-Zustellung: WriteAsync reicht die serialisierten Details an DispatchAsync durch — aber ein Channel bekommt sie nur, wenn eine Regel OpsAlertRule.IncludePayload setzt. Default AUS, bewusst: ein Ops-Alert ist ein "da ist was, schau nach"-Signal, die meisten Payloads sind im Chat Rauschen, und manche tragen Felder, die die Baseline nie traegt (Tenant-Domain, Contact-Adresse). Das Informationsniveau aller bestehenden Regeln bleibt damit unveraendert. ResolveTargets liefert deshalb zwei Gruppen. Ein Channel, den sowohl eine Payload-Regel als auch eine schlichte Regel adressiert, landet nur in der Payload-Gruppe: eine Nachricht pro Channel, und ein unbeteiligtes Geschwister-Match kann ein bewusstes Opt-in nicht aushebeln. Fuer den angereicherten Alert flacht BuildAlert die Top-Level-Skalare in Facts ab (max 12, Werte <= 256 Zeichen, Nulls und Verschachteltes raus), damit Card-Formate und der Telegram-Klartext dieselbe Information tragen wie der GenericJson-Consumer unter "data". EventType wird immer gesetzt — derselbe Wert steckt schon im "Event"-Fact, also keine zusaetzliche Information, nur ein stabiler Schluessel zum Verzweigen. Tests: TenantTrialStartedEventTests (8) — aufgezeichnete Event-Form inkl. Payload-Feldern, und die Emissionsbedingung des GA-Pfads gegen ApplyPlanChanges gepinnt (greift bei Grant und Verlaengerung, schweigt bei Trial-Ende, planTier-only und idempotentem Save). Die HTTP-Entry-Points bleiben ungetestet — Suite-Konvention ist, die reinen Kerne zu testen. Docs: OKF-Concept backend/ops-notification-channels.md. Registrierung in docs/index.md + docs/log.md fehlt bewusst — beide Dateien tragen noch fremdes uncommittetes WIP (ops-events Query-Surface), und ein Teil-Commit darin haette es mitgenommen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(backend,mcp): ops-events server-side field filters + generated MCP tool vocabularies The ops-events read could only narrow by category and date window, so answering "did any AgentEmergencyBreak fire yesterday?" meant pulling a whole category and filtering it client-side — and for the MCP tool that client is a language model, so the question cost a category-sized response in model context. Ops-events filter surface (mirrors the audit-log filter pattern one-for-one): - OpsEventQueryFilters (Shared) + OpsEventFilterRequest (query -> filters + pagination extras) + TableOpsEventRepository.AppendFieldFilters (filters -> OData clauses). BOTH filter builders call the appender — the single-category / unpaged one and the per-partition fan-out builder that adds the RowKey bound — so "no category named" cannot silently return unfiltered rows. - minSeverity expands to an OR-set, not a range: Table Storage compares strings lexicographically and the severity names sort Critical < Error < Info < Warning, so `Severity ge 'Warning'` would drop exactly the alert-worthy half. The ladder now lives in OpsEventSeverity.Rank, shared with OpsAlertDispatchService, so "Warning and above" in a read means what it meant when the alert rules fired. - severity is normalised onto the canonical vocabulary and an unknown value is a 400: `eq` is case-sensitive, so "?severity=warning" would otherwise return an empty page that reads exactly like "no such events happened". eventType stays verbatim — stored rows outlive the vocabulary, so a retired type must remain searchable in the historical window. - Every filter value is folded into the continuation fingerprint and echoed on nextLink; extras are appended AFTER the pre-existing category/tenantId discriminators and skipped when empty, so tokens minted before the filters existed still fingerprint identically. The tenantId drill deliberately stays in memory (OrdinalIgnoreCase; an OData eq would lose rows on casing). - MCP get_ops_events gains eventType / severity / minSeverity plus a `days` shorthand resolved client-side to a concrete dateFrom (a backend-side "last N days" would re-resolve "now" on every page and blow the token fingerprint); an explicit bound always wins over the shorthand. Fixed alongside: the paged cross-category fan-out skipped Platform (Azure Monitor alerts) because its category list had drifted from OpsEventCategory — written from day one, invisible to every paged reader, still visible on the unpaged full-table path. The list is now OpsEventCategory.All plus a reflection coverage test. That drift had a structural cause, so the same round closes it for the MCP as a whole: wire TYPES reached the MCP from day one, but vocabularies did not — types are erased at runtime and z.enum() needs values, so every tool schema hand-typed its own copy with nothing to compare against. The copies had drifted: search_sessions offered 5 of 8 session statuses (no AwaitingUser, no Incomplete — both real stored statuses), both event readers offered 4 of 6 severities (no Debug, no Trace), get_ops_events named 6 of 7 ops categories. - The codegen now emits src/generated/wire-vocabularies.generated.ts (VALUES: session statuses, event severities, ops categories/severities/types, annotation lanes/verdicts) from the manifest sections listed in MCP_VOCABULARIES; SharedManifestParityTests exports the two new ops vocabularies. Freshness is pinned twice — byte-equality against a fresh codegen run in the MCP suite, and the shared-manifests-in-sync CI job. - Tool schemas derive their z.enum() from it and descriptions interpolate the list instead of spelling it. - vocabulary-drift.guard.test.ts ratchets what is left: every inline z.enum([..]) must sit on a baseline naming why it has no backend owner (MCP-local knobs, plus rule type / connection type / CVE risk, which have no C# constants class yet). A new hand-typed list fails, and so does a stale baseline entry. - OpsEventTypes (Shared) gives the 77 ops event types a canonical home: the OpsEventService write sites use the constants (a raw literal now fails OpsEventTypeDualRegisterTests, which moved from regex-scraping to reflection), the manifest exports them, and get_resource(name="ops_event_types") serves categories + severities + types to the model from the generated copy. Suites: Backend 5427, MCP 629 + tsc, Web tsc + vitest 965, generator idempotent. Deploy order: backend BEFORE the MCP redeploy — an MCP that sends filters to an older backend gets silently unfiltered rows back and presents them as filtered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: register the ops notification channels doc in the index and log The concept doc landed with the feature commits; its index entry and log entry were still uncommitted in the working tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
73a6817713 |
build(deps): bump github/codeql-action from 4.37.3 to 4.37.9 (#138)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.37.3 to 4.37.9. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v4.37.3...v4.37.9) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.37.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
c04280cf68 |
feat(mcp): MCP server adopts the generated wire types — the codegen now writes a second copy of wire-types.generated.ts into McpServer/src/generated (MCP_HEADER, buildMcpWireTypesSource, same freshness contract), pinned by a new MCP vitest freshness test (repo-root discovery like event-types-drift, byte-equal to a fresh codegen run, mutation-proven red/green) and enforced in the CI shared-manifests-in-sync diff check; the hand-declared response shapes and structural inline casts in tools/*.ts now import the generated types on the INPUT side only (tool output contracts unchanged): sessions.ts (GetSessionResponse/GetSessionEventsResponse/GetRuleResultsResponse/GetSessionAnnotationsResponse/DiagnosticsDownloadTicketResponse; get_session_summary events are Partial<EnrollmentEvent>, RuleResult drops the dead ruleTitle??title fallback), admin.ts (usage union GetMcpUserUsage/GetGlobalMcpUsage/GetGlobalMcpUsageDaily, GeographicLocationSessionsLeanResponse, GetAllTenantConfigurationsResponse, QueryRawTableResponse, GetUnmatchedSoftwareResponse, SoftwareInventoryResponse; the client-side pagers pin their wire-critical records/summaries/inventory key names via satisfies keyof so a backend rename goes tsc-red instead of silently paging nothing), search.ts (GetSessionEventsResponse on both event readers, SearchSessionsResponse on the legacy fallback, RawEventsPage = Partial<Pick<QueryRawEventsResponse,...>>), rules.ts (DryRunAnalyzeRuleResponse envelope; the inner RuleDryRun trace stays a local reader because the wire type is deliberately unknown); the raw-session fallback in get_session_diagnostics/get_session_summary is removed — { success, session } IS the pinned contract, and the diag test now asserts a non-enveloped object surfaces as a loud tool error instead of being silently absorbed; rule-stats, global/metrics/platform and verdict-calibration keep local readers (backend still serializes anonymous objects there — no DTO exists to import); MCP suite 617 green, tsc green, generator idempotent
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d055eed074 |
Typed API contract: every success response is a wire DTO, generated end-to-end into TypeScript (#136)
* feat(backend): typed API response infrastructure — IApiResponse marker, wire-settings single source, widened anonymous-body ratchet (batch 0 of typed-api-contract) IApiResponse (Shared, with SuccessMessageResponse/SuccessOnlyResponse and the ProjectedItems/WireContract attributes for the upcoming manifest export) plus generic OkAsync/CreatedAsync/JsonAsync<T> overloads constrained on it, so anonymous success bodies stop compiling once the legacy object overloads go; ApiJsonOptions is now the single source of the production wire settings (camelCase, WhenWritingNull, string enums) used by Program.cs AND the three endpoint-test harnesses (two serialized PascalCase-with-nulls, one JsonSerializerDefaults.Web — all could pass while the deployed wire differed); TypedResponseGuardTests replaces OkAsyncBaselineGuardTests with the same ratchet mechanics over two frozen baselines (44 anonymous helper sites unchanged, plus 134 anonymous success WriteAsJsonAsync sites across 76 files — error-shaped bodies stay anonymous by design) and a flatness/location reflection guard (IApiResponse implementers derive from object and live in Shared, protecting JSON key order); ApiResponseWireParityTests carries the ordinal old-vs-new serialization proof each converted site must add Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(backend): Sessions/Annotations/Reports/Raw/Progress success responses are typed wire DTOs (batch 1 of typed-api-contract) 34 anonymous success bodies across 24 functions replaced with flat IApiResponse DTOs in Shared (SessionApiModels/ReportApiModels + new Annotation/Raw/ProgressApiModels) — minimal in-place transformation, call structure/status codes untouched, declaration order == wire order, nullable exactly where a site can emit null (WhenWritingNull); fields= projections stay IReadOnlyList<object> marked [ProjectedItems(SessionSummary/ EnrollmentEvent)], raw table rows stay PascalCase-verbatim dictionaries (no DictionaryKeyPolicy — pinned by test), AnnotationWire and the distress-report aggregations now build named item types instead of anonymous ones, and shared shapes collapse to one DTO (SessionListResponse x4, SessionAnnotationListResponse x2, SessionReportListResponse/GetSessionEventsResponse paged+unpaged via absent-null nextLink); 54 new wire-parity facts serialize each OLD anonymous literal (independent fixture copied from the pre-migration site) against the new DTO ordinally, with a null case per vanishing key; ratchet baselines shrink 44->14 helper sites and 134->122 write sites; the three distress-report test files read the now-typed aggregation items directly instead of via reflection-ToDynamic Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(backend): every anonymous API success body is a typed wire DTO — ratchet baselines empty, legacy object overloads deleted (batches 2-5 of typed-api-contract) the remaining 141 anonymous success sites (Admin 36, Rules 24, Config 15, Graph 5, Metrics 16, Vulnerability 17, Infrastructure/Global/Notifications/Bootstrap/ Diagnostics 20 incl. the 429 quota body in McpQuotaEnforcementMiddleware, plus the 14 helper-call sites) now return flat IApiResponse DTOs from Shared (new Admin/ Rules/Config/Graph/Metrics/Vulnerability/Infrastructure/Notifications/Diagnostics ApiModels; GeographicApiModels extended) — same minimal in-place transformation as batch 1: declaration order == wire order, nullable exactly where a site can emit null, dual success/failure sites keep their status-code logic and reuse SuccessMessageResponse, MCP-critical key names (records/summaries/inventory) pinned; wire item shapes that were function-local (CustomsArchive Run/EntrySummary, CpeMappingDto, UnmatchedSoftwareAgg, AutoResolve items, LogPatternTestResult, CveExposureSummary/SeverityBreakdown/TopCve — records rewritten as flat classes, netstandard2.0 has no IsExternalInit) moved to Shared so the manifest export sees them; payload types that must stay in the Functions assembly (RuleDryRun, PreviewWhitelistEntity, GlobalAdminEntity, HealthCheck, GlobalNotificationDto, Backfill/Reclassification/MaintenanceResult, TenantAdminEntity, TenantConfigFieldSchema) ride as object slots — wire-identical via runtime-type serialization, proven by the parity facts; 126 new wire-parity facts total (Admin 26, Rules 12, Config 17, Graph 7, Metrics 16, Vulnerability 19, Infrastructure 21 + Dictionary-key-verbatim and null-value pins); both TypedResponseGuard baselines are now EMPTY regression guards and the untyped OkAsync/CreatedAsync(object) overloads are gone, so a new anonymous success body no longer compiles through the helper and fails the guard through raw WriteAsJsonAsync; deliberately untouched: error bodies (one anonymous shape by design), auth/me (AuthDecisionResult.Body slot shared with error bodies, tests reflect over it), CheckMcpAccess (conditional dictionary keys), raw-byte streamers Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(contract): C#→TS wire codegen — shared-manifests v2 exports the full response-type graph as generated TypeScript (batch 6 of typed-api-contract) SharedManifestParityTests now writes schemaVersion 2: the v1 catalog sections stay byte-compatible (sectionFieldMap/signalrMessages/sharedManifestChecks untouched), the consumer-less sessionSummary section is gone, and a new types section carries every wire type — roots are all IApiResponse implementers plus [WireContract]-marked types (SlaViolatorSession, ImeVersionHistoryEntry), transitively closed by the new WireTypeManifestBuilder over refs, collections, dictionaries, enums and [ProjectedItems] item types (208 types). Fields honour [JsonPropertyName]/ [JsonIgnore], get-only members export readonly, a nullable slot exports optional (WhenWritingNull omits the KEY — explicit null survives only inside collections and dictionary values, so the nullable flag lives on nested descriptors only), field order follows STJ's derived-before-base serialization for the one legacy inherited wire type (LocationSessionRow : SessionSummary), and unmappable shapes (non-string dictionary keys, foreign BCL classes) fail the build instead of degrading to unknown. C# <summary> texts travel as JSDoc: Shared builds XML docs now (doc-quality warnings CS1591/1573/1574/1584/0419 suppressed — crefs to foreign assemblies cannot resolve from Shared; CS1570 malformed-XML stays an error, which surfaced a raw 0x01 control char and an unescaped ampersand in existing comments). generate-shared-manifest-types.js emits utils/wire-types.generated.ts (interfaces + string-union enums, 2.7k lines) next to the lean literal module, npm run generate:manifests wraps it, the vitest freshness test pins BOTH outputs, and a new shared-manifests-in-sync CI job (guardrails-in-sync pattern) fails on drift Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): hand-written response types re-export the generated wire interfaces (batch 7+8 of typed-api-contract) types/session.ts, types/enrollment.ts and types/adminConfig.ts are re-exports over utils/wire-types.generated.ts (Session = SessionSummary alias for the 25+ barrel importers; adminConfig keeps only the excessiveEventAutoActionMode literal-union narrowing and the client-side OpsAlertRule view) — the generated types are never weakened, consumers adapt: durationSeconds is now optional as on the wire (int? — consumers guard with ?? 0, which also retires the latent NaN-minutes display on in-progress sessions), status is the SessionStatus union (fixtures on canonical values; the generic string-sort test keeps its own fixture type), required bools break hand-built fixtures once → new test/factories.ts makeSession/makeEvent (Object.assign, not spread — Partial spread re-widens and tests must force data: undefined); the string|null lifecycle branches were already truthiness-based. Duplicate local types deleted: geographic-performance now uses the real LocationSessionRow wire type (incl. DO fields), GlobalSearch the generated QuickSearchResult, sla/page.tsx the generated SlaViolatorSession (status stays a number — C# int, rendered via statusLabels); sessionExportUtils aliases SessionExportEvent = EnrollmentEvent and derives RuleResultCsvData via Pick, CSV output unchanged; the three structural ReplayInputEvent clones (InstallEvent, ScriptInputEvent, DownloadEvent) are aliases now. docs: drift-guards.md replaces the shrink-only-OkAsync section with the typed-wire-DTO contract, index.md/log.md updated. tsc 0, vitest 957, eslint 0 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): SlaMetricsServiceTests anchor sessions inside the current ISO week — the 'now - 1h' fixture crossed into LAST week during the first hour of every Monday (UTC), emptying CurrentWeek and failing the three snapshot asserts (CI on Mon 2026-08-31 00:20Z; pre-existing Monday-morning time bomb, unrelated to the DTO migration); the anchor is now clamped to the week's Monday 00:00, proven green inside the same failure window Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
01311898a8 |
fix(ci): no third-party action holds release/deploy credentials by mutable tag — build-agent creates releases with first-party gh release create (job holds contents/id-token/attestations:write), combine-rules pushes generated outputs with plain git (contents:write on main), deploy-web builds the static export in CI (setup-node 22, npm ci, next build — same path ci.yml tests) and uploads it with the exactly pinned SWA CLI 2.0.10 instead of Azure/static-web-apps-deploy (Oryx in-container build, deploy token as action input), attests portal-<sha>.tar.gz and drops the unusable per-PR preview environments; azure/login and Azure/functions-action are SHA-pinned with version comments for Dependabot
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d0190f443d |
fix(ci): build-agent workflow_dispatch build_number can no longer inject PowerShell — the input and the agent blob SAS reach the version step only via env (BUILD_NUMBER_OVERRIDE, AGENT_SAS_TOKEN) instead of ${{ }} in the script body, and the override is validated against ^\d+$ before the [int] cast and the counter-blob CAS; counter semantics unchanged
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |