Skip to content

feat: add ability to dismiss discovered host suggestions#278

Open
devin-ai-integration[bot] wants to merge 3 commits into
mainfrom
devin/1781918818-dismiss-discovered-hosts
Open

feat: add ability to dismiss discovered host suggestions#278
devin-ai-integration[bot] wants to merge 3 commits into
mainfrom
devin/1781918818-dismiss-discovered-hosts

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the ability to dismiss discovered host suggestions from the Services page banner. Dismissed hosts are stored per-vault using the existing vault settings infrastructure (JSON array), so no database migration is required.

Backend:

  • DELETE /v1/vaults/{name}/discovered-hosts/{host} — dismiss a single host
  • DELETE /v1/vaults/{name}/discovered-hosts — dismiss all currently visible hosts
  • GET /v1/vaults/{name}/discovered-hosts now filters out previously dismissed hosts
  • Both DELETE endpoints enforce requireVaultAdmin (not just member)
  • Single-host dismiss is capped at 1000 entries per vault to prevent unbounded growth
  • All warn log lines include actor identity for incident correlation

Frontend:

  • Each host row gets an X (dismiss) button
  • Banner header gets a "Dismiss all" link
  • Both controls are admin-only, matching existing Add as service visibility
  • Dismiss failures surface error feedback via setError() instead of silently swallowing

Dismissed hosts persist across page reloads.

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Documentation
  • CI / build

Test plan

  • Existing tests pass (make test)
  • Added/updated tests for new behavior
  • Manual testing (describe below)

All existing discovered-hosts tests continue to pass (7/7). Go build and TypeScript typecheck both succeed. Manual UI testing confirmed dismiss single, dismiss all, and persistence across reloads.

Security checklist

  • No secrets or credentials in code
  • No new unauthenticated endpoints
  • Input validation on new API surfaces
  • Checked for OWASP top 10 (injection, XSS, etc.)

Both new endpoints use the same requireInitialized → requireAuth → actorAuthed middleware chain, with requireVaultAdmin enforced inside handlers. Dismissed hosts list is capped at 1000 entries per vault.

Link to Devin session: https://app.devin.ai/sessions/d95feaed4e29455aa99c9d8c31b9f5cc

Add dismiss functionality to the discovered hosts banner on the Services
page. Users can dismiss individual hosts via an X button or dismiss all
hosts at once via a 'Dismiss all' button in the banner header.

Backend:
- Store dismissed hosts as a JSON array in vault settings (no migration needed)
- Filter dismissed hosts from the GET discovered-hosts response
- Add DELETE /v1/vaults/{name}/discovered-hosts/{host} for single dismiss
- Add DELETE /v1/vaults/{name}/discovered-hosts for dismiss-all

Frontend:
- Add X dismiss button to each discovered host row
- Add 'Dismiss all' button in the banner header
- Both buttons are admin-only, consistent with existing 'Add as service'

Co-Authored-By: jake <jake@infisical.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@greptile-apps

greptile-apps Bot commented Jun 20, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds the ability to dismiss individual or all discovered-host suggestions from the Services page. Dismissed hosts are persisted per-vault via the existing vault settings key-value store (no migration required) and are filtered out on the next GET call.

  • Two new DELETE endpoints (/discovered-hosts/{host} and /discovered-hosts) persist a dismissed_discovered_hosts JSON array to vault settings; the frontend calls them from admin-gated buttons in the banner.
  • Both DELETE handlers enforce requireVaultMember rather than requireVaultAdmin, so any vault member can call the dismiss API directly even though the UI and PR description both state dismiss is admin-only.
  • handleDismissDiscoveredHost stores any arbitrary host string from the URL path without verifying it is an actual discovered host or capping the list size, leaving vault settings open to unbounded growth.

Confidence Score: 3/5

The dismiss write path has a real access-control gap that should be fixed before merging.

The two new DELETE handlers use member-level access control while the PR description and the UI both explicitly restrict dismiss to vault admins, meaning any member can bypass the intended restriction via direct API calls. The single-host dismiss endpoint also accepts and persists arbitrary host strings without validation or a size cap.

internal/server/handle_host_discovery.go — the authorization check and input validation on the new DELETE handlers need attention before this ships.

Important Files Changed

Filename Overview
internal/server/handle_host_discovery.go Adds dismiss-single and dismiss-all handlers for discovered hosts; uses requireVaultMember instead of the intended requireVaultAdmin, and stores arbitrary host strings without validation or a size cap.
internal/server/server.go Registers two new DELETE routes with the same middleware chain as the existing GET endpoint; straightforward and correct.
web/src/pages/vault/ServicesTab.tsx Adds per-host dismiss button and "Dismiss all" link gated behind isAdmin; errors are silently swallowed with no user feedback on failure.

Reviews (1): Last reviewed commit: "feat: add ability to dismiss discovered ..." | Re-trigger Greptile

return
}

if _, err := s.requireVaultMember(w, r, ns.ID); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Dismiss endpoints use member access instead of admin

The PR description explicitly states "Both controls are admin-only, matching existing Add as service visibility," and the UI correctly gates both dismiss buttons behind isAdmin. However, both new DELETE handlers call requireVaultMember instead of requireVaultAdmin. Any vault member — not just an admin — can call DELETE /v1/vaults/{name}/discovered-hosts/{host} or DELETE /v1/vaults/{name}/discovered-hosts directly via the API, silently bypassing the intended access control.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7e5fe0c — changed both handlers from requireVaultMember to requireVaultAdmin.

Comment on lines +173 to +175
dismissed := loadDismissedHosts(ctx, s.store, ns.ID)
dismissed[host] = true
if err := saveDismissedHosts(ctx, s.store, ns.ID, dismissed); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Arbitrary host strings are stored without validation or a size cap

handleDismissDiscoveredHost stores any host string from the URL path directly into vault settings without checking that the host actually exists in the discovered-hosts list. A vault member can dismiss thousands of fabricated host strings, causing the dismissed_discovered_hosts setting to grow unboundedly. Validating against the live discovered-host set (or capping the list) before persisting would prevent this.

Suggested change
dismissed := loadDismissedHosts(ctx, s.store, ns.ID)
dismissed[host] = true
if err := saveDismissedHosts(ctx, s.store, ns.ID, dismissed); err != nil {
const maxDismissed = 1000
dismissed := loadDismissedHosts(ctx, s.store, ns.ID)
if !dismissed[host] && len(dismissed) >= maxDismissed {
jsonError(w, http.StatusUnprocessableEntity, "Dismissed hosts limit reached")
return
}
dismissed[host] = true
if err := saveDismissedHosts(ctx, s.store, ns.ID, dismissed); err != nil {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7e5fe0c — added maxDismissedHosts = 1000 cap with a check before persisting.

return
}

jsonOK(w, map[string]any{"dismissed": len(dismissed)})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 dismissed count in dismiss-all response includes previously dismissed hosts

len(dismissed) reflects the total map size after merging all previously dismissed entries plus the newly dismissed ones, so the response value is misleading for API consumers who expect it to reflect how many hosts were dismissed in this call.

Suggested change
jsonOK(w, map[string]any{"dismissed": len(dismissed)})
jsonOK(w, map[string]any{"dismissed": len(unmatched)})

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7e5fe0c — response now reports len(unmatched) (newly dismissed count) instead of len(dismissed) (total map size).

Comment thread web/src/pages/vault/ServicesTab.tsx Outdated
Comment on lines +164 to +175
await apiFetch(
`/v1/vaults/${encodeURIComponent(vaultName)}/discovered-hosts/${encodeURIComponent(host)}`,
{ method: "DELETE" }
);
await fetchDiscoveredHosts(discoveredExpanded ? 100 : 5);
} catch {
// Degrade silently.
} finally {
setDismissingHost(null);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Silent error swallowing leaves the user with no feedback on failure

Both dismissDiscoveredHost and dismissAllDiscoveredHosts catch all errors and discard them. If the server returns a 500 or the network drops, the UI state and the server state will diverge: the host will remain in the server's discovered list but the user sees no indication that anything went wrong.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6f01df5 — both dismiss functions now check resp.ok and call setError(...) on failure, matching the pattern used by saveServices and deleteService in the same file.

dismissed := loadDismissedHosts(ctx, s.store, ns.ID)
dismissed[host] = true
if err := saveDismissedHosts(ctx, s.store, ns.ID, dismissed); err != nil {
s.logger.Warn("dismiss-host: save failed", "vault", vaultName, "err", err.Error())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Logging omits the requesting user's identity

The warn log for a failed dismiss save records vault and err but not the actor/user who made the request, making it harder to correlate incidents in cloud log services. The team's logging convention expects something like "user", actor.Username or the [username=…] format to appear on mutation-path log lines.

Rule Used: Follow a specific logging pattern for easier query... (source)

Learned From
Infisical/infisical#3651

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6f01df5 — both dismiss handlers now capture the actor from requireVaultAdmin and include "actor", actor.ID, "actor_type", actor.Type in all warn log lines.

Address Greptile review feedback:
- Change requireVaultMember to requireVaultAdmin for both DELETE handlers
- Add maxDismissedHosts (1000) cap to prevent unbounded growth
- Fix dismiss-all response to report count of newly dismissed hosts

Co-Authored-By: jake <jake@infisical.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Manual Test Results

Tested locally (Go server + Vite dev, SQLite seeded with 4 discovered hosts). Devin session

Dismiss Feature (5/5 passed)
  • Banner renders with dismiss controls — PASSED: "4 hosts detected in recent traffic" with "Dismiss all" + X buttons on each row
  • Dismiss single host (X button on api.github.com) — PASSED: Host removed, count updated to 3
  • Dismissed host persists after reload — PASSED: Still 3 hosts, sidebar badge updated
  • Dismiss all — PASSED: Banner completely disappears
  • Dismiss-all persists after reload — PASSED: Banner stays gone, no sidebar badge
Screenshots
Before After dismiss-all + reload
4 hosts No banner
4 hosts with Dismiss all + X buttons Banner gone, all hosts permanently dismissed

Also addressed Greptile review feedback in 7e5fe0c: upgraded requireVaultMemberrequireVaultAdmin for both DELETE endpoints, added maxDismissedHosts=1000 size cap, fixed dismiss-all response count.

- Show error feedback on dismiss failure instead of silently swallowing
- Add actor identity (ID + type) to dismiss log lines for incident correlation

Co-Authored-By: jake <jake@infisical.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants