Skip to content

[BUG] Observed images cannot be disabled or deleted, and discovery re-enables them #282

Open

Description

@LarsLaskowski

Summary

A user has no way to stop tracking an observed image: there is no disable/enable and no delete action anywhere in the UI, and the Docker Hub account discovery sets IsEnabled = true on every repository it lists on each run, so even a disabled own image would come back. A user who no longer cares about an image (an old manual registration, an abandoned repository in their Docker Hub account) keeps getting scans, findings, dashboard counts and shared-base-image entries for it. Decision (product manager): this must be corrected — the user can disable/enable and delete observed images in the UI, and discovery respects what the user decided.

Area

  • docs/areas/image-catalog.md (added in [Docs] Describe the current behavior by area #263), Observed images / Enabled state; also docs/areas/web-ui.md (pages and actions), docs/areas/scan-engine.md (scheduled observed-image scan), docs/areas/registries.md (account discovery), docs/areas/vulnerabilities.md (live image versions).
  • Security area: Database and migrations (entity model change, user-triggered delete built from a route/id value). The app has no authentication by design (guarantee in .squad/project.md); this issue does not change that.

Current behavior

  • ObservedImage.IsEnabled exists (src/DockerUpdateGuard.Data/Entities/ObservedImage.cs:28) but only two code paths write it:
    • discovery: src/DockerUpdateGuard/Images/DockerHubAccountImageDiscoveryService.cs:293-303 disables discovered images whose repository is no longer listed, and :220 / :234 set IsEnabled = true for every listed repository on every run:
      existingObservedImage.Name = repository.Repository;
      existingObservedImage.Description = NormalizeDescription(repository.Description);
      existingObservedImage.CurrentImageVersionId = imageVersion.Id;
      existingObservedImage.IsEnabled = true;
    • manual registration of an existing name: src/DockerUpdateGuard/Images/ImageRegistrationService.cs:90.
  • No page offers an action on an existing observed image: /observed-images (src/DockerUpdateGuard/Components/Pages/ObservedImages.razor) has only Register and scan; /my-images, /observed-images/{id} and /my-images/{id} have no buttons at all. The lists do not even show whether an image is enabled.
  • There is no delete path; ScanRuns.ObservedImageId and UpdateFindings.ObservedImageId are SetNull on delete (src/DockerUpdateGuard.Data/Configurations/ScanRunConfiguration.cs:52-55, UpdateFindingConfiguration.cs:70-73), so the schema would allow it.
  • Disabled images are skipped by the scheduled scan (src/DockerUpdateGuard/Images/ImageScanOrchestrator.cs:636) but still count everywhere else: the live image version set (src/DockerUpdateGuard.Data/Queries/LiveImageInventoryQueryService.cs:90, all observed images regardless of state), the shared base images view (see [BUG] Shared base images view lists unshared bases and counts disabled observed images #286), dashboard counts (src/DockerUpdateGuard/UI/ApplicationViewService.cs:1753-1754).

Failure scenario: the Docker Hub account contains myuser/old-experiment. The user cannot remove it from My images; it is scanned on every schedule, its vulnerabilities are refreshed and shown, and its base images appear on /base-images. Even if the row were disabled in the database by hand, the next discovery run (DockerHubAccountImageDiscoveryService.cs:234) enables it again.

Expected behavior

Decision (product manager): the user can disable, enable and delete observed images in the UI; discovery must not re-enable a user-disabled image and must not re-create a user-deleted discovered image.

States

An observed image is in exactly one of these states:

State Meaning Set by
Enabled Tracked: scanned, part of the live image versions, counted registration, discovery (new repository), user Enable/Restore
Disabled by user Not tracked; history kept; shown in the lists, marked Disabled user Disable
Missing from account (discovered images only) Repository no longer listed in the Docker Hub account; not tracked; history kept discovery (as today)
Ignored (discovered images only; tombstone) The user deleted a discovered image. Not tracked, hidden from the normal lists, never re-created or re-enabled by discovery user Delete on a discovered image

A user Delete of a manual image removes the row (hard delete); nothing re-creates manual images, so no tombstone is needed.

Tracking rules for every non-enabled state

  • Not scanned by the scheduled observed-image scan (as today) and not by own image base refresh.
  • Excluded from the live image version set (so the vulnerability refresh skips it and the full-refresh cleanup deactivates its findings unless the same image version is live through another image or a running container).
  • Excluded from the shared base images view and from dashboard counts of my images / manual observed images / protected assets.
  • History (scan runs, findings, relationships) is kept; catalog entries are never deleted.

UI scenarios

  1. Disable (manual or own image): the lists /observed-images and /my-images get a Status column (Enabled, Disabled, Missing from account) and a row action Disable; the detail pages /observed-images/{id} and /my-images/{id} get a Disable button. Disabling takes effect immediately (no confirmation), shows a snackbar "'' is disabled and will no longer be scanned", and the row stays in the list with status Disabled.
  2. Enable: on a disabled image (by user or missing from account) the action reads Enable. Enabling sets the image to Enabled and immediately starts a manual observed-image scan of it (like Register and scan). Enabling an image that is Missing from account is allowed; the next discovery run sets it back to Missing from account if the repository is still not listed (discovery is the source of truth for whether the repository exists).
  3. Delete a manual image: row action and detail-page button Delete, with a confirmation dialog ("Delete ''? Its scan history stays, findings are deactivated. This cannot be undone."). On confirm the observed image is removed, its active update findings (derived base runtime findings) are deactivated first, scan runs keep their history with the observed image reference cleared (existing SetNull), and the user is returned to /observed-images. Opening the old detail URL afterwards shows the existing not-found handling (see F-036 (P5) — Detail pages conflate loading and not-found state (perpetual spinner for a missing resource) #40).
  4. Delete a discovered (own) image: same confirmation, with the text "Delete ''? It will be ignored by the Docker Hub account discovery until you restore it." The image becomes Ignored: it disappears from /my-images and from all counts. /my-images gets a collapsed section Ignored repositories (shown only when there are any) listing name, repository and the time it was ignored, with a Restore action.
  5. Restore an ignored image: sets it to Enabled (or Missing from account if discovery last saw the repository missing — the next discovery run decides) and starts a manual scan, like Enable.
  6. Register a manual image whose reference matches a disabled manual image re-enables it (explicit user action; see [BUG] Manual image registration identifies images by name and can take over discovered images #284). Registration never touches discovered images.
  7. Actions are disabled while the page is busy (as the registration form is today); errors are shown in the page's existing error area.

Discovery rules (per account run)

Repository listed? Observed image state Result
yes none exists create, Enabled (as today)
yes Enabled move current version to the most recently pushed tag, refresh name/description (as today)
yes Missing from account set Enabled, then as above (as today)
yes Disabled by user leave state unchanged; no tag lookup, no catalog write (the version is moved when the user enables it and the next run happens)
yes Ignored skip completely; no tag lookup, not re-created
no Enabled set Missing from account (today: IsEnabled = false)
no Disabled by user / Ignored / Missing from account unchanged

The completion log keeps its counts; skipped user-disabled and ignored repositories are counted separately from failed repositories.

Acceptance criteria

  • The entity model distinguishes the four states (see Implementation notes) with a migration named per AGENTS.md (next is Update8); existing rows map to Enabled (IsEnabled = true) or Missing from account (IsEnabled = false, all such rows today were disabled by discovery).
  • A service (e.g. IObservedImageManagementService with DisableAsync, EnableAsync, DeleteAsync, RestoreAsync) implements the transitions; unknown ids fail with a clear message; deleting/disabling an already deleted/disabled image is a no-op.
  • /observed-images, /my-images and both detail pages offer the actions and status described in UI scenarios; delete asks for confirmation.
  • Discovery follows the table in Discovery rules; in particular it never sets a Disabled by user image to enabled and never re-creates an Ignored repository.
  • Non-enabled images are excluded from the scheduled scan, the own-image base refresh, the live image version set, the shared base images view and the dashboard counts.
  • Unit tests, e.g. DockerHubAccountImageDiscoveryServiceSynchronizeUserDisabledImageStaysDisabled, DockerHubAccountImageDiscoveryServiceSynchronizeIgnoredRepositoryIsNotRecreated, DockerHubAccountImageDiscoveryServiceSynchronizeMissingImageIsEnabledAgain, ObservedImageManagementServiceDeleteManualImageRemovesRowAndDeactivatesFindings, ObservedImageManagementServiceDeleteDiscoveredImageMarksIgnored, ObservedImageManagementServiceEnableStartsManualScan, LiveImageInventoryQueryServiceGetLiveImageVersionIdsExcludesDisabledObservedImages; render tests for the new actions/columns (ObservedImagesRenderTests/MyImages…Tests, following the existing …RenderTests / …PersistentStateTests patterns).
  • docs/ARCHITECTURE.md (Data layer, UI section) is updated.
  • The area document docs/areas/image-catalog.md describes the new behavior and its Known gaps entry for this issue is removed; docs/areas/web-ui.md, docs/areas/registries.md, docs/areas/scan-engine.md and docs/areas/vulnerabilities.md (live image versions) are updated accordingly.

Implementation notes

  • Model, recommended: keep IsEnabled (all existing scan queries keep working) and add ObservedImageStateReason (or similar) as an int-persisted enum: None = 0 (enabled), DisabledByUser = 1, MissingFromAccount = 2, Ignored = 3, plus an optional StateChangedAtUtc. Invariant: IsEnabled == (Reason == None). Alternative: replace IsEnabled with a single state enum — cleaner but touches every query that filters on IsEnabled; not recommended for this fix. A separate tombstone table keyed by registry/repository is also possible but duplicates the identity; the in-row Ignored state is simpler because discovery already loads all discovered rows (DockerHubAccountImageDiscoveryService.cs:286-291).
  • Discovery matches rows by registry/repository of the current version (MatchesRepository, :101-118); keep that, and make sure the per-repository update (SynchronizeRepositoryAsync, :169-239) returns early for Disabled by user / Ignored before calling GetTagsAsync.
  • Delete of a manual image: deactivate active UpdateFindings of the image (set IsActive = false, resolution timestamp) before removing the row, otherwise they stay active with a null ObservedImageId. Use an injected TimeProvider for new timestamps (docs/UNIT_TESTS.md, Time in tests).
  • An observed-image scan already returns early when the image is missing (ImageScanOrchestrator.cs:686-695); a scan that starts on a just-disabled image may finish — acceptable (single-instance, duplicate work is harmless per .squad/project.md).
  • Dashboard protected assets and counts (ApplicationViewService.cs:1753-1754, :1812-1813), OwnImageBaseRefreshBackgroundService.cs:56, LiveImageInventoryQueryService.cs:90 and SharedBaseImageQueryService must filter on IsEnabled.
  • Inventory telemetry gauges (ApplicationTelemetry) may keep counting all rows; if they are changed, update docs/areas/telemetry.md.
  • Do not change: no authentication is added (guarantee); IImageCatalogRepository stays the only catalog writer; catalog entries are never deleted.
  • No configuration change.

Related


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions