Files
disco-ad-compare-plugin/.claude-context/sessions/2026-04-28-ad-compare-plugin-build.md

6.7 KiB

Disco AD Compare Plugin - Session: Initial Build & Iteration

Date: 2026-04-28 Project: disco-ad-compare-plugin Status: ongoing Tags: disco-ict, plugin, active-directory, c-sharp, managed-by, uiextension

Context

Built a Disco ICT plugin from scratch that compares the AD computer managedBy attribute against the Disco AssignedUserId for devices, and reports mismatches. The plugin was developed iteratively through many build-fix cycles due to constraints of the target environment (no Visual Studio, only .NET Framework 4.0 MSBuild, Disco installed at C:\Program Files\Disco\WebApp\bin).

Key Decisions

  • C# 5 only — The server has only .NET Framework MSBuild v4.0.30319 (no VS installed). No string interpolation ($""), no null-conditional (?.), no auto-property initializers, no expression-bodied members. All code uses string.Format(), explicit null checks, constructors for list init.
  • ToolsVersion 4.0 / TargetFramework v4.5 — Downgraded from 15.0/4.7.2 to avoid MSB3644 errors. The warnings about architecture mismatch (MSIL vs AMD64) are harmless.
  • Manual manifest.json — No ManifestGenerator.exe available in the install. Build script creates manifest manually with correct field names matching PluginFeatureManifest.cs: Id, Name, TypeName, PrimaryFeature, CategoryTypeName.
  • CategoryTypeName for UIExtension — Must be the generic type definition: Disco.Services.Plugins.Features.UIExtension.UIExtensionFeature``1 (backtick-1 for one generic param). Confirmed by comparing with working plugins (CasperSuite, PasswordManager, DocFill).
  • DN-to-DN comparison — AD managedBy returns a Distinguished Name, not DOMAIN\username. Must look up the Disco assigned user in AD to get their DN, then compare DN-to-DN (case-insensitive). Initial version compared DOMAIN\username against DN which always mismatched.
  • Filter invalid devices — Skip devices without DOMAIN\ComputerName format (no backslash) to avoid "The Id must include the Domain" errors. Also exclude devices not found in AD from the results table (counted separately).
  • Server-side rendering for web handler — jQuery/AJAX approach failed because the standalone plugin page doesn't load Disco's layout/scripts. Rewrote to render full HTML server-side with inline CSS. The /Run action generates the complete page.
  • Plugin URL routing — Disco route is Plugin/{PluginId}/{PluginAction} where PluginAction is required. URL must be /Plugin/Disco.Plugins.ADCompare/Run not /Plugin/Disco.Plugins.ADCompare.
  • Build as .zip then rename — Compress-Archive only supports .zip extension. Build script creates .zip then renames to .discoPlugin.
  • HintPath regex replacement — Single regex <HintPath>[^<]+\\([^\\<]+\.dll)</HintPath> replaces ALL assembly references to point at DiscoBinPath, handling Disco, Newtonsoft, MVC, WebPages, Razor, and EntityFramework in one pass.

Technical Details

Build Command

.\Build-Plugin.ps1 -DiscoBinPath "C:\Program Files\Disco\WebApp\bin"

Disco Plugin Paths

  • Installed plugins: C:\Disco\Plugins\<PluginId>\
  • Plugin packages: C:\Disco\PluginPackages\
  • Plugin storage: C:\Disco\PluginStorage\
  • Disco binaries: C:\Program Files\Disco\WebApp\bin\
  • Disco logs: CSV format, viewable in Disco web UI under Configuration

Key Disco API Patterns

  • ActiveDirectory.RetrieveADMachineAccount(domainId, additionalProperties) — returns AD computer, null if not found
  • ActiveDirectory.RetrieveADUserAccount(userId) — looks up by DOMAIN\username
  • adAccount.GetPropertyValue<T>("attributeName") — reads AD attributes like managedBy, lastLogonTimestamp
  • UIExtensionFeature<DeviceShowModel> — inject HTML on device pages
  • UIExtensionFeature<UserShowModel> — inject HTML on user pages
  • PluginWebHandler.ExecuteAction(ActionName) — web handler routing
  • PluginConfigurationHandler with WebViewPage — required for plugin system

Assembly References Required

System, System.Core, System.Data, System.Data.Entity, System.Web, System.ComponentModel.DataAnnotations, EntityFramework, System.Web.Mvc, System.Web.WebPages, System.Web.Razor, Newtonsoft.Json, Disco.Models, Disco.Data, Disco.Services

Manifest Feature Format (exact JSON shape)

{
  "Id": "ADCompareDeviceUI",
  "Name": "Device Page - AD Compare",
  "TypeName": "Disco.Plugins.ADCompare.Features.DeviceUIExtension",
  "PrimaryFeature": false,
  "CategoryTypeName": "Disco.Services.Plugins.Features.UIExtension.UIExtensionFeature`1"
}

Code Changes

  • ADComparePlugin.cs — Main plugin class, empty Initialize (features self-register via manifest)
  • Features/DeviceCompareService.cs — Core comparison: DN-to-DN matching, filters invalid/missing devices
  • Features/DeviceUIExtension.cs — Device page panel: managedBy comparison, last login, AD status
  • Features/UserUIExtension.cs — User page panel: last login, AD status, OU location, 30-day inactivity warning
  • Models/DeviceComparisonResult.cs — Result + summary models with SkippedDevices counter
  • WebHandler/ADCompareWebHandler.cs — Server-side rendered dashboard at /Run, CSV export at /Export
  • ConfigurationHandler/ADCompareConfigurationHandler.cs — Required config handler with WebViewPage
  • Properties/AssemblyInfo.cs — Version 1.3.1.0, author Jess Rogerson
  • Build-Plugin.ps1 — Build + package script, regex HintPath resolution, manual manifest generation
  • Disco.Plugins.ADCompare.csproj — ToolsVersion 4.0, .NET 4.5, all references
  • .gitignore — Standard .NET ignores
  • README.md — Full documentation

Open Questions / Next Steps

  • UIExtension panels not rendering — Features initialize without errors in Disco logs (Initialized Plugin Feature: 'DeviceUIExtension'), manifest format matches working plugins exactly, but HTML panels don't appear on device/user pages. The Register() call in Initialize() override runs. Need to investigate: does Literal() work? Does Nothing() suppress? Should we check how CasperSuite's CasperSuiteUIExt_DeviceShow.ExecuteAction returns its result vs our approach? May need to examine the Disco Views (.cshtml) that render UIExtension slots to understand where the HTML is injected.
  • Comparison dashboard works perfectly — /Plugin/Disco.Plugins.ADCompare/Run renders correctly with summary stats, mismatch table, and CSV export.
  • Credit line — Plugin should show "Created by: Jess Rogerson (yelling commands at Claude.AI)" in a footer or about section.

Artifacts