diff --git a/CLAUDE.md b/CLAUDE.md index b8b5842..e5ed5ac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -48,7 +48,7 @@ All of `docs/*.html` were reviewed against the code on 2026-08-02 and are curren - **Secrets:** `ISecretResolver.cs`/`SecretResolver.cs`, `SecretWriter.cs`, `UpdateSecretCommand.cs` (the `update-secret` CLI — dispatch, arg parsing, silent/upgrade flows), `InteractiveConfigPrompts.cs` (console prompt primitives for the interactive flow, incl. masked input), `ImpersonationRunner.cs` (`LogonUser` + `RunImpersonated` for Credential-Manager/User-DPAPI writes; isolates the native `extern` surface), `TpmSecretProtector.cs`, `AgentSecretFile.cs` (`-secret @` off-argv), `SecretRedactor.cs` (plain/URL-encoded/XML-escaped/Base64 forms, longest-first), `CertificateThumbprintValidator.cs` - **Agent process:** `AgentProcessLauncher.cs` (the `IAgentProcessLauncher`/`IAgentProcess` seam — production wraps `System.Diagnostics.Process`; fakes drive the watchdog in tests), `AgentArgumentParser.cs`, `AgentTransport.cs` (pure transport-selection: `InitialMethod`/`Toggle`/`ShouldFallback`), `JavaPathResolver.cs`, `ServiceSettingsValidator.cs` - **Networking:** `IJarDownloader.cs`/`HttpJarDownloader.cs` (conditional GET; temp-file + atomic move so a truncated download can't sit behind a valid validator; records the jar's SHA-256 on download and re-verifies the cached jar on each 304 — a mismatch forces an unconditional re-download. TOFU/local-integrity, not upstream authenticity — that's `Connection:ControllerCertThumbprint`), `JarCacheValidator.cs` (the freshness token: `ETag` → `If-None-Match` preferred, else `Last-Modified` → `If-Modified-Since`. **a controller may send `Last-Modified` and no `ETag`** (observed on a live one), and against those an ETag-only cache never populates — every start re-downloads the full jar. That was issue #80. One file per kind, ETag-sidecar and Modified-sidecar; writing either deletes the other. A stored value is screened for control characters and size-capped before it is replayed — it goes out via `TryAddWithoutValidation`, and the cache dir is writable by the account build steps run as), `JarCachePaths.cs` (cache-directory paths derived once, passed as one value), `AgentJar.cs` (**the** definition of `agent.jar` + the `.etag`/`.modified`/`.sha256` sidecar names — the downloader writes and the worker launches the same file, so don't re-declare it), `IConnectivityChecker.cs`/`TcpConnectivityChecker.cs`, `ProxyResolver.cs` (`ProxyMode` + `Connection:Proxy`/`ProxyBypass` parsing; bypass entries become **whole-URI** anchored regexes) -- **Hardening:** `ProcessMitigations.cs`, `EnvironmentSanitizer.cs`, `ConfigAclHardener.cs`, `DataDirectoryAcl.cs` + `GrantDataAccessCommand.cs` and `ServiceRecoveryConfigurator.cs` + `ConfigureRecoveryCommand.cs` (the `grant-data-access` / `configure-recovery` verbs the MSI calls as EXE custom actions in place of the WiX Util extension — see the installer note below. `DataDirectoryAcl` is deliberately the **inverse** of `ConfigAclHardener`: the config file's DACL is replaced with inheritance *off* because it may hold the secret, the data folder's is added to with inheritance *on* so it keeps its inherited SYSTEM/Administrators full control. Both are idempotent — they run on install, upgrade **and** repair), `EventLogSourceInstaller.cs`, `DataPaths.cs` (binary/data split; `ComputeDataDirectory` is the **non-creating** resolve — cleanup must not re-create the tree it is deleting), `UninstallCleanup.cs` + `PurgeCommand.cs` + `SecretPurger.cs` (the top-level `purge` verb — **not** a flag on `update-secret`, which updates no secret: deletes the data tree, the config, **and the secret from its store** — TPM key / Credential Manager entry / machine env var, since only `Unprotected`+`Dpapi` keep the material in the config and the rest store a pointer. `SecretStoreNames.cs` holds those names **once**, shared with `SecretWriter` — two copies could drift and uninstall would report success while leaving a usable credential. `SecretPurger.Purge` returns a `SecretStoreOutcome` so callers/tests assert the result, not log wording; `IsSafeToDelete` refuses drive roots, non-fully-qualified paths and system folders — this is a recursive delete running as SYSTEM off an operator-supplied path) +- **Hardening:** `ProcessMitigations.cs` + `MitigationLevel.cs`, `EnvironmentSanitizer.cs`, `ConfigAclHardener.cs`, `DataDirectoryAcl.cs` + `GrantDataAccessCommand.cs` and `ServiceRecoveryConfigurator.cs` + `ConfigureRecoveryCommand.cs` (the `grant-data-access` / `configure-recovery` verbs the MSI calls as EXE custom actions in place of the WiX Util extension — see the installer note below. `DataDirectoryAcl` is deliberately the **inverse** of `ConfigAclHardener`: the config file's DACL is replaced with inheritance *off* because it may hold the secret, the data folder's is added to with inheritance *on* so it keeps its inherited SYSTEM/Administrators full control. Both are idempotent — they run on install, upgrade **and** repair), `EventLogSourceInstaller.cs`, `DataPaths.cs` (binary/data split; `ComputeDataDirectory` is the **non-creating** resolve — cleanup must not re-create the tree it is deleting), `UninstallCleanup.cs` + `PurgeCommand.cs` + `SecretPurger.cs` (the top-level `purge` verb — **not** a flag on `update-secret`, which updates no secret: deletes the data tree, the config, **and the secret from its store** — TPM key / Credential Manager entry / machine env var, since only `Unprotected`+`Dpapi` keep the material in the config and the rest store a pointer. `SecretStoreNames.cs` holds those names **once**, shared with `SecretWriter` — two copies could drift and uninstall would report success while leaving a usable credential. `SecretPurger.Purge` returns a `SecretStoreOutcome` so callers/tests assert the result, not log wording; `IsSafeToDelete` refuses drive roots, non-fully-qualified paths and system folders — this is a recursive delete running as SYSTEM off an operator-supplied path) - **Other:** `Properties/AssemblyInfo.cs` (explicit assembly attributes — required for Codacy; do not delete) `src/JenkinsAsService.Installer/` — WiX v5 MSI (`.wxs` files + `License.rtf`). `tests/JenkinsAsService.Tests/` — xUnit suite. @@ -78,6 +78,7 @@ Settings live under the `Jenkins` section of `appsettings.json`, grouped into su - **`Connection:Proxy`/`ProxyBypass`** cover the **.NET jar download only** — the Java agent's own controller connection is the JVM's and needs `-Dhttps.proxyHost=…` via `Agent:CustomArguments`. Empty = system proxy, `direct`/`none` = bypass, else an address (bare `host:port` gets `http://`). Parsed at startup so a bad value fails loudly instead of looking like an unreachable controller. `ProxyBypass` entries become **anchored URI-shaped regexes** — `WebProxy.BypassList` is regex (not wildcards) and matches the *whole URI*, so a host-only pattern silently never matches. - **`Logging:DebugMode` is live.** `LogLevelController` (hosted service) re-points a Serilog `LoggingLevelSwitch` from `IOptionsMonitor`; everything else is still startup-only. Restarting to enable debug logging kills the agent and destroys the state being diagnosed, which is the whole point. - **Config is pinned to the install folder**, not the process working directory: `BuildHost` adds `AddJsonFile(Path.Combine(basePath, ...), reloadOnChange: true)`. The host's default content root is the CWD, which for an SCM-started service is `%SystemRoot%\system32` — without this the bound settings come back empty and the service dies claiming `Connection:Url` is missing *while naming a config file that is fully populated*. Don't remove it; the `reloadOnChange` is also what makes DebugMode live. +- **`Hardening:ProcessMitigations`** (`Full` default / `AllowNetworkImages` / `Off`) is a dial on what the **service** applies to itself, because mitigation policies are **inherited by child processes** and Windows gives no way to drop one for a child that the parent holds. `PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY` looks like the answer and is not: its `ALWAYS_OFF` bits override the *system default*, not an inherited policy - measured on 11 26200, parent at `image-load = 7`, child created with every `ALWAYS_OFF` bit still reads 7, while the same attribute list with `ALWAYS_ON` against a clean parent correctly yields 7 (so the attribute is honoured; inheritance simply wins). Don't re-propose launching the agent with cleared mitigations. Only `NoRemoteImages` has build-visible consequences - a build step loading tooling or a native DLL from a UNC path or mapped drive - so it gets its own level rather than forcing all-or-nothing; the wizard shows the three **horizontally as radio buttons, not a drop-down**, so an operator whose build just broke can see the middle option exists instead of reaching for `Off`. The old code comment claiming mitigations "affect only this process, never the spawned `java.exe` child" was **wrong**; that was issue #82. Also measured there: extension-point-disable is already on by default and `SetProcessMitigationPolicy` returns `ERROR_ACCESS_DENIED` for it, so that warning at startup is expected noise. - Key axes: `Connection:Method` (`Auto`/`WebSocket`/`Https` — `Https` means direct TCP inbound, not literal HTTPS), `Secret:Mode` (`Unprotected`/`Dpapi`/`Tpm`/`EnvironmentVariable`/`CredentialManager`), `Secret:ViaFile`, `Secret:DpapiScope`, `Hardening:SanitizeEnvironment`, `Agent:DataDirectory`, `Recovery:MaxRetries` (0 = infinite). - Runtime data lives in `%ProgramData%\JenkinsAsService`, **separate** from the read-only install folder in `Program Files`. Within it: logs + secret file at the root, cached `agent.jar` (+ `.etag` or `.modified`, and `.sha256`) under `agent\`, and the Jenkins `-workDir` under `work\` — the cache is isolated from workspace churn so a build step can't clobber the binary the watchdog launches. `DataPaths.ResolveAgentDirectory`/`ResolveWorkDirectory` derive the subdirs; don't put the jar back in the workdir root. - On MSI upgrade, `update-secret --upgrade` reconciles `appsettings.json` to the schema (add-missing + prune-unknown) via `ServiceSettingsNormalizer`, preserving existing values and the secret; the secret-presence check is decryption-free/identity-independent (raw-string read of `Secret:Value`, works as SYSTEM regardless of which service identity the secret is bound to). diff --git a/README.md b/README.md index a0c974a..10c96c6 100644 --- a/README.md +++ b/README.md @@ -297,7 +297,7 @@ dotnet build src/JenkinsAsService.Installer -c Release ` - TLS 1.2+ enforced by default (.NET 10), with optional controller certificate pinning (`ControllerCertThumbprint`) - Secrets encrypted at rest (TPM 2.0 hardware-backed key, DPAPI machine/user scope, or CredMgr), redacted from all logs, and passed to the agent off the command line via `-secret @` so they never appear in the process table -- Least-privilege virtual service account, deny-by-default environment block for the agent child, and Win32 process-mitigation policies (no remote/low-IL/non-System32 DLL loads, extension-point injection disabled) +- Least-privilege virtual service account, deny-by-default environment block for the agent child, and Win32 process-mitigation policies (no remote/low-IL/non-System32 DLL loads, extension-point injection disabled). Mitigation policies are **inherited by the build tree**; `Hardening:ProcessMitigations` selects how much is applied - Binary/data separation: read-only binaries in `Program Files`, writable runtime data in `ProgramData` — a malicious pipeline can't overwrite the service `.exe`. The cached `agent.jar` (+ SHA-256) lives in an `agent\` subfolder isolated from the build `work\` dir and is integrity-checked before each launch, so a build step can't swap the binary the watchdog runs - Deterministic builds with locked NuGet restore and embedded PDB symbols - CycloneDX **SBOM** generated in CI and attached to every release (with SHA-256 checksum) diff --git a/Security.md b/Security.md index 1d5483e..df4a728 100644 --- a/Security.md +++ b/Security.md @@ -98,7 +98,7 @@ At runtime the resolved secret is written to a separate ACL-restricted file and ### Process and Filesystem Hardening - The Java agent child is launched with a **deny-by-default environment** — only a curated allow-list plus explicitly configured additions — so the service's own environment block cannot leak into untrusted pipeline scripts -- Win32 **process-mitigation policies** are applied to the service process: no remote, low-integrity or non-System32 DLL loads, and legacy extension-point injection disabled +- Win32 **process-mitigation policies** are applied to the service process: no remote, low-integrity or non-System32 DLL loads, and legacy extension-point injection disabled. Windows **inherits these into child processes**, so they cover `java.exe` and the build tree beneath it too, and that cannot be split — a child cannot hold fewer mitigations than its parent. `Hardening:ProcessMitigations` therefore tunes what the *service* takes: `Full` (default), `AllowNetworkImages` (drops only the UNC/network DLL block, the one with build-visible consequences), or `Off`. See [Process mitigations](https://jenkinsasservice.machlev.org/configuration.html#process-mitigations) for the measurement behind that constraint - **Binaries and runtime data are separated**: the install folder stays read-only to the agent identity, while logs, the secret file, the jar cache and the build work directory live under `%ProgramData%`. A malicious pipeline therefore cannot overwrite the service binary and wait for a restart - The cached `agent.jar` lives in its own subfolder, isolated from the build workspace, and its **SHA-256 is re-verified** before reuse — trust-on-first-use local integrity, complementing controller certificate pinning for upstream authenticity - The service runs under a least-privilege **virtual service account** (`NT SERVICE\Jenkins`) by default, and the runtime data folder is granted to that account explicitly (inheritable `Modify`, with inheritance left on so SYSTEM and Administrators keep full control) diff --git a/docs/api-reference.html b/docs/api-reference.html index ffe8cd9..c2217cc 100644 --- a/docs/api-reference.html +++ b/docs/api-reference.html @@ -312,7 +312,7 @@

Hardening

ProcessMitigations Apply(warn?) - Sets Win32 mitigation policies on the service process: no remote, low-integrity or non-System32 DLL loads, and legacy extension-point injection disabled. Affects future LoadLibrary calls in this process only, not the Java child. Best-effort — never blocks startup. + Sets Win32 mitigation policies on the service process at the level given by Hardening:ProcessMitigations: no remote, low-integrity or non-System32 DLL loads, and legacy extension-point injection disabled. These policies are inherited by the Java child and everything the build spawns beneath it, which is what makes the level configurable — see Process mitigations. Best-effort — never blocks startup. EnvironmentSanitizer @@ -361,7 +361,7 @@

Program.cs

  • CLI dispatchCliCommands.TryResolve(args, out var command) maps args[0] to a handler and exits; anything unrecognised falls through to service mode. Deliberately before Serilog init, since Environment.Exit skips the flush block. The verb table lives in CliCommands rather than inline so it can be tested: an unregistered verb still compiles, and its only symptom is the exe silently starting in service mode — which, for a verb invoked by an MSI custom action, is a failed install. CliCommandsTests asserts by reflection that every command type declaring a Name is dispatchable. The four verbs are update-secret, purge, grant-data-access and configure-recovery.
  • Boot config — a throwaway ConfigurationBuilder reads Logging:DebugMode, Logging:CompactLog, Logging:RetainedLogs and Agent:DataDirectory before the host exists.
  • Serilog — rolling file sink in the data directory (10 MB, RetainedLogs kept; agent.log or agent.clef), Event Log sink at Warning+ when the source is usable, enrichers ProcessId / MachineName / EnvironmentName, and level overrides muting Microsoft, System.Net.Http and Polly below Warning.
  • -
  • ProcessMitigations.Apply — hardens the service process itself.
  • +
  • ProcessMitigations.Apply — hardens the service process, and by inheritance everything it launches.
  • HostBuildHost(args, basePath), split into four single-purpose steps:
    • Config sourceAddJsonFile(Path.Combine(basePath, "appsettings.json"), reloadOnChange: true), then AddWindowsService(ServiceName = "Jenkins") and Configure<ServiceSettings>.
    • diff --git a/docs/architecture.html b/docs/architecture.html index a495aba..75d2976 100644 --- a/docs/architecture.html +++ b/docs/architecture.html @@ -75,7 +75,7 @@

      Project Structure

      │ │ ├── ProxyResolver.cs # Connection:Proxy/ProxyBypass parsing │ │ ├── CertificateThumbprintValidator.cs # Controller cert pinning │ │ ├── AgentProcessLauncher.cs # IAgentProcessLauncher/IAgentProcess seam -│ │ ├── ProcessMitigations.cs, EnvironmentSanitizer.cs, ConfigAclHardener.cs, +│ │ ├── ProcessMitigations.cs, MitigationLevel.cs, EnvironmentSanitizer.cs, ConfigAclHardener.cs, │ │ │ EventLogSourceInstaller.cs # Hardening │ │ ├── DataDirectoryAcl.cs, GrantDataAccessCommand.cs, # data-folder ACL grant, and │ │ │ ServiceRecoveryConfigurator.cs, # the SCM failure actions - both @@ -408,7 +408,7 @@

      Testing

      CertificateThumbprintValidatorTests / ProcessMitigationsTests / DataPathsTests - Thumbprint normalization/pinning match, mitigation policy application, data-directory resolution/expansion + Thumbprint normalization/pinning match, mitigation flag set per level, data-directory resolution/expansion diff --git a/docs/configuration.html b/docs/configuration.html index 637f7df..e28d6c6 100644 --- a/docs/configuration.html +++ b/docs/configuration.html @@ -156,6 +156,12 @@

      Settings Reference

      (empty) Extra environment variable names (semicolon/comma-separated, case-insensitive) to pass through to the agent when Hardening:SanitizeEnvironment is on. Example: GRADLE_USER_HOME;MAVEN_OPTS. + + Hardening:ProcessMitigations + No + Full + How much of the Win32 process-mitigation subset the service applies to itself: Full, AllowNetworkImages, or Off. These policies are inherited by the build tree — see Process mitigations. + Logging:DebugMode No @@ -208,7 +214,8 @@

      JSON Format

      }, "Hardening": { "SanitizeEnvironment": true, - "AllowedEnvironmentVariables": "" + "AllowedEnvironmentVariables": "", + "ProcessMitigations": "Full" }, "Logging": { "DebugMode": false, @@ -445,6 +452,34 @@

      Secret off the command line

      Environment sanitization

      With Hardening:SanitizeEnvironment: true (default) the Java agent is launched with a deny-by-default environment: only a curated allow-list of variables the JVM and common Windows build tooling need (SystemRoot, PATH, TEMP, JAVA_HOME, ProgramFiles, …) is passed through. This stops the service's own environment block — which may carry host secrets or tokens — from being inherited by untrusted pipeline scripts that run inside the agent. If a build needs an extra host variable, add it to Hardening:AllowedEnvironmentVariables (semicolon/comma-separated, case-insensitive) rather than disabling the feature.

      +

      Process mitigations

      +

      At startup the service applies a narrow subset of Win32 process-mitigation policies to itself: block DLL loads from UNC/network paths, block DLLs written by low-integrity processes, prefer %WinDir%\System32 for known system DLLs, and disable legacy extension-point injection.

      +

      Process mitigation policies are inherited by child processes. They therefore reach java.exe and every compiler, test runner and script the agent spawns beneath it — this is not confined to the service. Windows offers no way to undo the inheritance for a particular child: a child can be given additional mitigations at creation time through PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, but its ALWAYS_OFF bits override only the system default, not a policy inherited from the parent (measured on Windows 11 26200: with the parent at image-load = 7, a child created with every ALWAYS_OFF bit still reads back 7). Sparing the build tree a mitigation therefore means not applying it in the service either, which is why this is one dial rather than a separate service and child setting.

      +

      Of the mitigations applied, only the network-image block has build-visible consequences: a build step that runs tooling, or loads a native DLL, from a UNC path or a mapped drive fails under the service in a way it does not fail when run by hand, and the failure surfaces as a loader error inside the build that names nothing to do with this service. That is the one AllowNetworkImages drops.

      + + + + + + + + + + + + + + + + + + + + + +
      LevelAppliesWhen to use
      FullAll of the aboveDefault. Keep it unless a build actually breaks.
      AllowNetworkImagesEverything except the network-image blockA build loads tools or native DLLs from a UNC path or mapped drive. Preferred over Off — it gives up one protection, not all of them.
      OffNothingLast resort, for a pipeline broken by a mitigation AllowNetworkImages does not cover. The service logs a warning at every start while this is set.
      +

      The level is also offered on the installer's Advanced Options page.

      +

      Data directory split (binaries vs. runtime data)

      The service keeps its read-only binary and its writable runtime data in two separate locations:

      @@ -473,6 +508,7 @@

      Data directory split (binaries vs. runtime data)

      CLI: update-secret

      The JenkinsAsService.exe update-secret subcommand writes (or re-writes) appsettings.json with the secret processed for the chosen mode. Used by the MSI installer and manually.

      +

      An option given a value it cannot use — a misspelled level, a non-numeric count — is a hard error: the command reports every bad option, writes nothing, and exits 1. An option given an empty value is "unset" and is simply not applied, which is what an untouched installer field sends. The distinction matters because these run as MSI custom actions: a typo fails the install instead of quietly producing a service configured differently from what was asked for.

      JenkinsAsService.exe update-secret [options]
       
         Secret input (pick one)
      @@ -495,6 +531,7 @@ 

      CLI: update-secret

      --custom-args <value> Agent:CustomArguments --sanitize-env <bool> Hardening:SanitizeEnvironment (default: true) --allowed-env <names> Hardening:AllowedEnvironmentVariables (; or , separated) + --mitigations <level> Hardening:ProcessMitigations (Full | AllowNetworkImages | Off) --debug <bool> Logging:DebugMode --compact-log <bool> Logging:CompactLog — CLEF JSON output --retained-logs <n> Logging:RetainedLogs (default: 3) diff --git a/docs/overview.html b/docs/overview.html index 7c1e2cd..09a2955 100644 --- a/docs/overview.html +++ b/docs/overview.html @@ -74,7 +74,7 @@

      Key Benefits

    • Event-driven watchdog — detects agent death instantly via process.Exited / TaskCompletionSource, auto-recovers with exponential backoff (10s→300s)
    • Smart jar caching + integrity — ETag-based conditional GET (If-None-Match / 304 Not Modified) skips the download when the jar is unchanged; the jar's SHA-256 is recorded on download and re-verified before reuse, so a tampered or corrupt cached jar is re-downloaded instead of launched
    • Secret protection — TPM 2.0 hardware-backed key, DPAPI (machine/user scope), Windows Credential Manager, system env var, or plaintext. Passed to the agent off the command line via -secret @<file> (ACL-restricted) so it never appears in the process table. CLI subcommand update-secret with --impersonate support
    • -
    • Process hardening — least-privilege virtual service account, deny-by-default environment block for the agent child, and Win32 mitigation policies (no remote/low-IL/non-System32 DLL loads, extension-point injection disabled)
    • +
    • Process hardening — least-privilege virtual service account, deny-by-default environment block for the agent child, and Win32 mitigation policies (no remote/low-IL/non-System32 DLL loads, extension-point injection disabled) — these are inherited by the build tree, and tunable via Hardening:ProcessMitigations
    • Binary/data separation — read-only binaries in Program Files, writable runtime data (jar, logs, secret, work dir) in ProgramData — the agent identity can't overwrite its own .exe
    • HTTP resilience — Polly-based retry, circuit breaker, and timeout via Microsoft.Extensions.Http.Resilience
    • SEVERE crash diagnostics — counts SEVERE: lines per agent run; logs the count on each exit
    • diff --git a/docs/search-index.json b/docs/search-index.json index e4036a5..aa7413c 100644 --- a/docs/search-index.json +++ b/docs/search-index.json @@ -127,6 +127,11 @@ "url": "configuration.html#secret-off-argv", "keywords": ["secret via file", "secret off command line", "process table", "get-process", "wmi commandline", "secret file"] }, + { + "title": "Process mitigations", + "url": "configuration.html#process-mitigations", + "keywords": ["process mitigations", "mitigation policy", "allow network images", "unc dll", "mapped drive", "noremoteimages", "image load policy", "inherited by child", "build breaks", "hardening level"] + }, { "title": "Environment sanitization", "url": "configuration.html#env-sanitization", diff --git a/src/JenkinsAsService.Installer/ConfigDialog.wxs b/src/JenkinsAsService.Installer/ConfigDialog.wxs index 07100a4..f28343a 100644 --- a/src/JenkinsAsService.Installer/ConfigDialog.wxs +++ b/src/JenkinsAsService.Installer/ConfigDialog.wxs @@ -202,24 +202,52 @@ + + + + + + + + + + - - - - - + - - + - + diff --git a/src/JenkinsAsService.Installer/Package.wxs b/src/JenkinsAsService.Installer/Package.wxs index cfd8a9f..7c5b1a6 100644 --- a/src/JenkinsAsService.Installer/Package.wxs +++ b/src/JenkinsAsService.Installer/Package.wxs @@ -23,6 +23,7 @@ + @@ -49,6 +50,7 @@ + @@ -90,6 +92,8 @@ Sequence="both" Condition="NOT JENKINS_RETAINED_LOGS" /> + @@ -157,7 +161,7 @@ Execute="deferred" Impersonate="no" />