Skip to content

Report installer prerequisite failures to Sentry - #263

Open
anonpran wants to merge 13 commits into
stagingfrom
nanda/installer-failure-reporting
Open

anonpran wants to merge 13 commits into
stagingfrom
nanda/installer-failure-reporting

Conversation

@anonpran

@anonpran anonpran commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Make the Windows installer report why it gave up, instead of exiting silently.

The gap

The agent is what reports. Every exit above it is invisible:

install.ps1
  ├─ looks for Python
  │     missing -> exit 1        <- nothing recorded anywhere
  ├─ downloads the repo
  │     failed  -> exit 1        <- nothing recorded anywhere
  └─ runs the agent              <- reporting starts only here

Git was a hard Windows prerequisite from 4 Feb until #262. It printed Git is not installed. to a console nobody reads. The only visible symptom was 69 Xome devices that checked in via MDM and had no discovery row, with nothing to explain them. Python is the remaining gate of the same shape.

The change

Send-InstallerFailure posts a Sentry event before each of those exits:

tags: phase=installer_blocked  reason=no-python  domain=...  branch=main
      os=Windows  hostname=...  device_id=<serial, best-effort>  ps_version=...
title: Discovery installer blocked: no-python
extra: { detail: "<the real exception>" }

It lands in the same project as DISCOVERY-TOOL-SCRIPT-17, so blocked installers sit next to zero-tool scans where these get investigated.

Reason codes are no-python and repo-download-failed. The latter carries the real exception text, so a certificate failure, a proxy 403 and a missing Expand-Archive are three different answers rather than one shrug.

Why it can report at all

install.ps1 already makes an authenticated HTTPS call a few lines earlier, for the branch lookup. Network and TLS are proven on that machine before either gate. The capability was always there; it was never used.

Design notes

Sentry, not the scan lifecycle endpoint. The first version of this PR posted scan_event=failed to /api/v1/ai-tools/report/. That made device_id a primary key, which meant the installer had to resolve device identity exactly as the agent does or write a duplicate device_discovery row. That single requirement produced ~30 lines of probe logic, a PowerShell copy of INVALID_SERIAL_VALUES, and five review findings. As a tag, device_id cannot corrupt anything, so all of it went away and constants.py is a single definition again.

Low-cardinality title. The reason code is in the title, the varying detail is in extra. The discovery.dispatch.lock errors put a username in the message and split one bug into 44 issues, which is a large part of why it went unnoticed for months.

Fails silently by design. Wrapped, 10s cap, $null = on the call. This runs on an already-broken install and must not change the exit code or add delay.

No PII beyond existing practice. Reason codes, versions, hostname and serial. The agent already tags hostname on every event.

AI_DISCOVERY_SENTRY_DSN overrides the DSN; empty disables sending. Matches how the agent is silenced in tests.

Testing

tests/test_install_ps1_archive_fallback.py     6 passed, 2 skipped

Cross-platform cases in the class that already owns this file. The gate-coverage test walks back from each exit's error text and asserts a report precedes it, so a third gate added later without reporting fails the build.

Every guard mutation-tested:

report removed from a gate  -> test_every_prerequisite_exit_reports_before_quitting  FAILED
$Detail in the title        -> test_issue_title_carries_only_the_reason_code          FAILED
DSN guard removed           -> test_sentry_dsn_can_be_disabled_for_tests              FAILED

No pwsh on my machine, so CI on windows-latest is the first real execution of the PowerShell — worth a reviewer's eye on syntax.

Not in scope

install.sh has the same Python gate. Worth saying plainly: the Git bug existed because the two installers drifted after the Feb archive-fallback change touched only install.sh. Doing Windows alone repeats that, so this should not sit long.

The warnings are untouched. "No active user logged in. Discovery may return 0 results." predicts an empty scan in plain English and we never hear it either. That belongs on the scan the agent is about to start, so it needs a different mechanism.

🤖 Generated with Claude Code

https://claude.ai/code/session_01A7m2NkfcYw536ZbmPJ7kfu

Greptile Summary

The PR adds persistent Windows installer diagnostics and reports prerequisite failures to Sentry, while adding scan-event phase tags to distinguish backend delivery failures.

  • Writes installer output to the shared Unbound log directory with bounded and redacted detail.
  • Reports missing arguments, unavailable Python, and repository-download failures as Sentry events.
  • Preserves scan-event phase context when backend delivery errors are reported.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains.

No blocking failure remains.

Important Files Changed

Filename Overview
install.ps1 Adds local installer logging and Sentry reporting for prerequisite failures.
scripts/coding_discovery_tools/utils.py Preserves scan-event-specific phase context when reporting backend delivery failures.
tests/test_install_ps1_archive_fallback.py Adds static coverage for installer reporting, event shape, metadata, logging, redaction, and DSN disabling.
tests/test_send_and_persist.py Verifies that Sentry diagnostics distinguish scan lifecycle messages from inventory delivery.

Reviews (13): Last reviewed commit: "Scrub third-party exception text before ..." | Re-trigger Greptile

Context used:

  • Context used - P0 — Critical (must block merge) Django / Backend ... (source)

install.ps1 exits before the discovery agent starts when Python is missing
or the repository cannot be downloaded. The agent is what reports to the
backend, so those devices produce no device_discovery row, no device_scans
row and no Sentry event. They are indistinguishable from a machine that was
never enrolled.

Git was a hard Windows prerequisite from 4 Feb until #262 yesterday. It
printed "Git is not installed." to a console nobody reads, and the only
symptom was 69 Xome devices that had checked in via MDM and had no discovery
row, with nothing to explain them. Python is now the remaining gate of that
shape.

Send-InstallerFailure posts the existing scan lifecycle event
(scan_event=failed) to /api/v1/ai-tools/report/, so the backend creates the
device and scan rows it already knows how to create and the failure becomes a
SQL question. No backend change. The script has already made an authenticated
call to this host by that point, for the branch lookup, so network and
credentials are proven.

Uses Invoke-RestMethod for the Windows certificate store, matching the branch
lookup above it and the archive download in #262; curl is the equivalent rule
on the Python side. Swallows every error and caps at 10s: this runs on an
already-broken install and must not change the exit code or add delay.
Reports codes and versions, never usernames or hostnames.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7m2NkfcYw536ZbmPJ7kfu
@anonpran
anonpran requested a review from a team September 2, 2026 04:47
Comment thread install.ps1 Outdated
Comment thread install.ps1 Outdated
Comment thread install.ps1

@vigneshsubbiah16 vigneshsubbiah16 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛡️ Automated Security Review (consensus)

5 findings — 1 high-confidence, 4 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.

🔴 HIGH — API key sent over cleartext HTTP

install.ps1:67

  • Impact: When Domain is explicitly http://, the credentialed failure-report POST sends Authorization: Bearer without encryption; the branch lookup above already blocks non-HTTPS, so this is the only path that can leak the org API key on the wire.
  • Fix: Gate the POST on $_domain -match '^https://' (or normalize to HTTPS) before attaching the Bearer header — match the branch-lookup guard.
  • Flagged by: Claude, Cursor (Greptile inline)

🟡 TRIAGE — Download exception text may carry PII into scan_error

install.ps1:129 → install.ps1:218

  • Impact: $script:LastDownloadError = $_.Exception.Message is forwarded as scan_error.message; .NET web/IO exceptions often embed full URLs (incl. query tokens), temp paths (C:\Users\<user>\...), and proxy hostnames — stored in device_scans contrary to the PR's "no PII" posture; COMPUTERNAME fallback is also a hostname.
  • Fix: Send a bounded, sanitized message (fixed reason text + exception type, or strip URLs/paths and truncate); document hostname fallback if intentional.
  • Flagged by: Claude, Cursor

🟡 TRIAGE — Placeholder BIOS serial can mis-attribute devices

install.ps1:51

  • Impact: Nonblank placeholder serials (e.g. To be filled by O.E.M., 0) are accepted as device_id while the normal agent rejects them and falls back to hostname — multiple machines can collapse under one ID or diverge from later scan identity.
  • Fix: Reuse the agent's serial validation / hostname fallback before posting.
  • Flagged by: Cursor (Greptile inline)

🟡 TRIAGE — TLS protocol set widened with -bor instead of assigned

install.ps1:64

  • Impact: -bor Tls12 keeps weaker protocols already enabled in the process; an active MITM could negotiate a downgrade on this credentialed POST (low practical risk on modern Windows defaults).
  • Fix: Assign explicitly, e.g. [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 (add Tls13 where supported).
  • Flagged by: Claude

🟡 TRIAGE — Bearer token may follow redirects cross-host

install.ps1:67

  • Impact: Invoke-RestMethod follows redirects by default; on Windows PowerShell 5.1 the Authorization header can be retained to a different host, forwarding the API key on misconfigured or hostile redirects.
  • Fix: Add -MaximumRedirection 0 to the report call (or validate final host matches Domain).
  • Flagged by: Claude

Previously acknowledged (not re-flagged)

  • Silent outer catch on reporting — PR: fails silently by design; must not change exit code, raise a second error, or add delay on an already-broken install.
  • Undiagnosable reporting failures — same accepted tradeoff; operators cannot distinguish a recorded prerequisite failure from a failed report attempt.

🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 49fdd5b6 · 2026-09-02T04:52Z

Two real defects from Greptile, both from not mirroring contracts that
already exist a few lines away.

The branch lookup at the top of the file guards on `-match '^https://'`
before attaching the Bearer key, and normalization only prepends https when
the domain has no scheme — an explicit `-Domain http://...` survives. The
report attached the same key with no such guard, so an http domain would have
put it on the wire in clear. Same guard now.

Device id came straight from Win32_BIOS with no validation, while the agent
runs it through is_valid_serial and falls back to the hostname. A machine
reporting "To Be Filled By O.E.M." would have created one device_discovery
row shared by every machine of that model, and it would not have matched the
row a later successful scan writes. Get-DeviceId now applies the same
INVALID_SERIAL_VALUES list with the same hostname fallback.

Both covered by tests that fail when the guard or a list entry is removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7m2NkfcYw536ZbmPJ7kfu
@cursor

cursor Bot commented Sep 2, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@anonpran

anonpran commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Both findings were real and are fixed in 61ba347. Neither was a judgement call — I failed to mirror contracts that already exist a few lines away in the same file.

HTTPS guard. The branch lookup does exactly this before attaching the key:

if ($_domain -and $_key -and $_domain -match '^https://') {

and normalization only prepends https when the domain has no scheme, so an explicit -Domain http://... survives it. My POST carried the same Bearer key with no such check. Same guard now applied:

if (-not $_key -or -not $_domain -or $_domain -notmatch '^https://') { return }

Device identity. Worse than described, I think. I took Win32_BIOS.SerialNumber raw, while WindowsDeviceIdExtractor runs it through is_valid_serial() and falls back to hostname. On a machine reporting To Be Filled By O.E.M. that would have created a single device_discovery row shared by every machine of that model, and it still would not have matched the row a later successful scan writes. New Get-DeviceId mirrors INVALID_SERIAL_VALUES and the hostname fallback.

Both are covered by tests that fail when the guard or any list entry is removed:

guard removed          -> test_failure_report_never_sends_the_key_over_http     FAILED
one serial removed     -> test_installer_rejects_the_same_placeholder_serials   FAILED
both present           -> 6 passed, 2 skipped

The serial test imports INVALID_SERIAL_VALUES from constants.py and asserts set equality, so the two lists cannot drift.

Still worth a reviewer's eye on the PowerShell syntax — no pwsh on my machine, so CI on windows-latest is the first real execution.

Comment thread install.ps1 Outdated
Get-DeviceId dropped to the hostname after a single failed CIM query, while
WindowsDeviceIdExtractor tries Get-WmiObject and two wmic forms first. A
machine where CIM fails but WMI or wmic answers would file its installer
failure under the hostname and its later scan under the BIOS serial, so the
two would land on different device_discovery rows. That is the exact split
this PR exists to prevent.

Now walks the same four probes in the same order with the same validation,
and only reaches COMPUTERNAME when all of them fail, which is where the agent
lands too. Test asserts each probe is present and that they all precede the
hostname fallback.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7m2NkfcYw536ZbmPJ7kfu
@anonpran

anonpran commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Valid, fixed in f0adb82.

I'd cut the chain to one probe. The agent runs four before giving up:

Get-WmiObject Win32_BIOS
wmic bios get serialnumber /format:list
wmic bios get serialnumber
get_hostname()

Mine went CIM -> hostname. So on a box where CIM fails but Get-WmiObject or wmic answers, the installer files under the hostname and the later scan files under the BIOS serial. Two device_discovery rows for one machine, which is precisely the split this PR exists to prevent.

Worth noting Get-CimInstance and Get-WmiObject are different stacks, not aliases, so this is not purely theoretical.

Get-DeviceId now walks the same four probes in the same order with the same validation, and only reaches COMPUTERNAME when all four fail, which is where the agent lands too.

wmic probes removed  -> test_device_id_probes_every_source_the_agent_does   FAILED
present              -> 7 passed, 2 skipped

The test also asserts every probe precedes the hostname fallback, so reordering breaks the build rather than silently splitting rows again.

@vigneshsubbiah16 vigneshsubbiah16 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛡️ Automated Security Review (consensus)

3 findings — 2 high-confidence, 1 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.

Raw exception text can persist paths/PII in scan_error

install.ps1:146, install.ps1:235 · 🔴 HIGH
Impact: $script:LastDownloadError = $_.Exception.Message is sent verbatim as scan_error.message; download/extraction errors often embed C:\Users\<username>\... paths, hostnames, or request URIs (including signed query params if the archive URL carries them).
Fix: Post a stable reason code or exception type only; if a message is needed, scrub \Users\ segments and query strings and cap length — add a test that rejects \Users\ in the payload.
Reviewers: Claude, Lead

Legacy TLS protocols remain enabled for authenticated requests

install.ps1:83 · 🔴 HIGH
Impact: SecurityProtocol -bor Tls12 adds TLS 1.2 but leaves SSL 3.0 / TLS 1.0 enabled process-wide, weakening negotiation for the branch lookup, archive download, and failure-report POST (all carry the Bearer key).
Fix: Assign [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 (optionally -bor Tls13 inside a try/catch on newer .NET) instead of OR-ing into the existing set.
Reviewers: Claude, Lead

Installer failure may report under a different device_id than the agent

install.ps1:51-58 · 🟡 TRIAGE
Impact: Get-DeviceId falls back to $env:COMPUTERNAME when CIM fails, but the agent can still recover the BIOS serial via WMIC — prerequisite failures and later scans may land on separate device rows.
Fix: Extend Get-DeviceId with the agent's WMIC fallback (or share one implementation) so failure reports target the same row as successful scans.
Reviewers: Greptile

Previously acknowledged (not re-flagged)

  • Silent suppression of report errors (install.ps1 outer catch) — fails silently by design; must not alter exit code, surface secondary errors, or add delay on an already-broken install.
  • Bearer key sent over explicit http:// domain — fixed in 61ba347; maintainer applied the same ^https:// guard used by the branch lookup.
  • Placeholder BIOS serial used as device_id — fixed in 61ba347; Get-DeviceId now mirrors INVALID_SERIAL_VALUES and hostname fallback, with tests tied to constants.py.

🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 61ba3474 · 2026-09-02T05:02Z

Comment thread install.ps1 Outdated
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7m2NkfcYw536ZbmPJ7kfu
Comment thread install.ps1 Outdated
I had added it ahead of the agent's own first probe. CIM and WMI are separate
client stacks over the same class, so a machine where they disagree would have
had its installer failure filed under the CIM answer and its scan under the
WMI one. Adding a probe diverges just as much as omitting one.

The three callers all launch install.ps1 through powershell.exe, never pwsh,
so Get-WmiObject is always available and there is nothing for the extra probe
to cover.

Test now asserts Get-CimInstance is absent and that the remaining probes
appear in the agent's order, so neither a new probe nor a reorder passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7m2NkfcYw536ZbmPJ7kfu
@anonpran

anonpran commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Valid, fixed in ac0028a — and this time by deleting code rather than adding it.

I'd put Get-CimInstance ahead of the agent's own first probe. The agent starts at Get-WmiObject and never uses CIM at all. Those are separate client stacks over the same class, so on a machine where they disagree the installer files under the CIM answer and the scan files under the WMI one. Adding a probe diverges exactly as much as omitting one, which is the thing I was trying to fix two commits ago.

Get-DeviceId is now the agent's chain verbatim:

{ (Get-WmiObject Win32_BIOS -ErrorAction Stop).SerialNumber },
{ (& wmic bios get serialnumber /format:list) ... },
{ (& wmic bios get serialnumber) | Select-Object -Skip 1 }
-> $env:COMPUTERNAME

I checked the reason I'd reached for CIM in the first place — Get-WmiObject not existing under PowerShell 7. It doesn't apply: all three callers launch install.ps1 through powershell.exe, never pwsh.

  • mdm/onboard.py:264 — ["powershell", "-NoProfile", ...]
  • setup-scheduled-scan.ps1:245 — & powershell -NoProfile ...
  • the SessionStart hook — System32\WindowsPowerShell\v1.0\powershell.exe

So there was nothing for the extra probe to cover.

The test now asserts Get-CimInstance is absent and that the remaining probes appear in the agent's order, so neither a new probe nor a reorder gets through:

Get-CimInstance re-added  -> test_device_id_probes_match_the_agent_exactly   FAILED
as-is                     -> 7 passed, 2 skipped

Thanks for staying on this one — all four findings have been the same class of mistake, me reimplementing something the agent already does and diverging from it. That is also the root cause of the bug this PR exists to fix.

Comment thread install.ps1 Outdated

@vigneshsubbiah16 vigneshsubbiah16 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛡️ Automated Security Review (consensus)

3 findings — 0 high-confidence, 3 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.

🟡 Unsanitized download exception posted as scan_error.message

install.ps1:150 (also consumed at :238)
Impact: $script:LastDownloadError forwards raw .Exception.Message, which can embed request URIs (incl. query strings), proxy hosts, or local temp paths — widening exposure beyond the local console to anyone with report/DB access.
Fix: Sanitize before send (strip query strings, cap length) or post only exception type + HTTP status; keep full text in the existing Write-Warning.
Flagged by: Claude, Lead

🟡 Process-wide TLS protocol weakened via -bor assignment

install.ps1:85
Impact: OR-ing Tls12 into the existing SecurityProtocol leaves legacy protocols enabled on older .NET defaults, and on .NET 4.7+ can override SystemDefault and block TLS 1.3 — affecting this POST and the archive download in the same process.
Fix: Assign explicitly (Tls12 | Tls13 with try/catch for missing Tls13), or drop the line and rely on SystemDefault like the branch lookup already does.
Flagged by: Claude, Lead

🟡 Hostname fallback contradicts stated “no PII” boundary

install.ps1:59
Impact: When BIOS probes fail, device_id is $env:COMPUTERNAME, which may encode usernames/employee IDs — inconsistent with the PR’s “device serial only, not hostnames” claim (matches agent behavior, but changes the privacy posture of failure reports).
Fix: Update the PR/docs to acknowledge hostname fallback, or hash the fallback in both installer and WindowsDeviceIdExtractor together.
Flagged by: Claude

Previously acknowledged (not re-flagged)

  • Cleartext HTTP Bearer key on failure report — fixed in 61ba347; HTTPS guard now mirrors branch lookup (@anonpran).
  • Placeholder BIOS serial accepted as device_id — fixed in 61ba347; INVALID_SERIALS now mirrors constants.py (@anonpran).
  • Device-identity probe mismatch (CIM/extra probes, wrong order) — fixed in f0adb82 / ac0028a; Get-DeviceId now matches agent chain verbatim (@anonpran).
  • Empty outer catch hides reporting failures — accepted by design; must not alter exit code or delay on an already-broken install (PR description: “Fails silently by design”).

🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head ac0028a1 · 2026-09-02T05:11Z

The probes returned arrays and the caller stringified them, so PowerShell
joined every line with a space. wmic emits a header, blank lines, and one row
per BIOS instance, so a machine with more than one usable line would have
reported "ABC123 DEF456" as its serial while the agent reported "ABC123" —
a serial belonging to no machine, on a row the later scan never touches.

The agent strips each line, drops the empties, skips the header and returns
the first valid one. The loop now does the same, which also removes the need
for each probe to normalise its own output.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7m2NkfcYw536ZbmPJ7kfu
@cursor

cursor Bot commented Sep 2, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@anonpran

anonpran commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Valid, fixed in edb8888.

The probes return arrays and the caller did "$(& $probe)".Trim(), which is a PowerShell array-to-string join on space. wmic emits a header, blank lines, and one row per BIOS instance, so a machine with more than one usable line would have reported "ABC123 DEF456" while the agent reported "ABC123".

Worse than a mismatch — that is a serial belonging to no machine at all, on a row no scan will ever touch again.

The agent's shape is: strip each line, drop empties, skip the header, return the first valid one.

foreach ($line in $lines) {
    $serial = "$line".Trim()
    if ($serial -and $INVALID_SERIALS -notcontains $serial.ToUpper()) { return $serial }
}

Moving the line-splitting into the loop also means each probe no longer has to normalise its own output, so the three probes stay literal translations of the agent's three.

reverted to the join  -> test_device_id_probes_match_the_agent_exactly   FAILED
as-is                 -> 7 passed, 2 skipped

@vigneshsubbiah16 vigneshsubbiah16 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛡️ Automated Security Review (consensus)

0 findings — 0 high-confidence, 0 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.

No open security issues. Send-InstallerFailure mirrors the branch-lookup HTTPS guard, keeps the Bearer token out of the JSON body, and Get-DeviceId now follows the agent's probe chain, placeholder-serial rejection, and first-valid-line parsing. Semgrep and Gitleaks are clean.

Previously acknowledged (not re-flagged)

  • HTTP report could send API key over cleartext (install.ps1) — Fixed in 61ba347; maintainer added the same ^https:// guard used by the branch lookup.
  • Placeholder BIOS serial submitted as device_id (install.ps1) — Fixed in 61ba347; maintainer aligned INVALID_SERIALS and hostname fallback with constants.py / the agent.
  • Device-ID probe chain diverged from the agent (CIM extra probe, missing WMI/WMIC steps) (install.ps1) — Fixed in f0adb82 and ac0028a; maintainer replaced the custom chain with the agent's four-step sequence verbatim.
  • WMIC multi-line output joined into a bogus serial (install.ps1) — Fixed in edb8888; maintainer now takes the first valid line per probe, matching the agent.
  • Empty catch blocks make reporting failures undiagnosable (install.ps1) — Accepted by design; PR author: reporting must fail silently on an already-broken install (no exit-code change, no added delay).

🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head edb88883 · 2026-09-02T05:30Z

device_id is a primary key on the scan lifecycle endpoint, so the installer
had to resolve device identity exactly as the agent does or it would write a
second device_discovery row for the same machine. That single requirement
produced Get-DeviceId, a PowerShell copy of INVALID_SERIAL_VALUES, and five
review findings, all of them subtle and all in the same function.

As a Sentry tag, device_id cannot corrupt anything, so none of that is
needed. Get-DeviceId and the constant copy are gone and constants.py is a
single definition again. The serial is now a best-effort tag; a placeholder
value is information rather than a collision.

The event lands beside DISCOVERY-TOOL-SCRIPT-17 in the project where these
failures are already investigated, tagged phase=installer_blocked with the
reason, domain, branch and PowerShell version. The title carries the reason
code only, so the issue does not fragment per machine the way the
dispatch.lock errors did.

Traded away: joining blocked machines to their org's device rows in SQL. The
domain tag identifies the customer and device_id the machine, which answers
the same question without a key that has to be exactly right.

AI_DISCOVERY_SENTRY_DSN overrides the DSN and an empty value disables
sending, matching how the agent is silenced in tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7m2NkfcYw536ZbmPJ7kfu
@anonpran anonpran changed the title Report installer prerequisite failures to the backend Report installer prerequisite failures to Sentry Sep 2, 2026
@anonpran

anonpran commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Reworked the target after review. Failures now go to Sentry, not the scan lifecycle endpoint.

The five findings on this PR were all in Get-DeviceId, and they existed for one reason: device_id is a primary key on that endpoint, so the installer had to resolve identity exactly as the agent does or it would write a second device_discovery row for the same machine.

As a Sentry tag, device_id cannot corrupt anything. So the requirement disappears, and with it Get-DeviceId, the PowerShell copy of INVALID_SERIAL_VALUES, and three identity tests. constants.py is a single definition again.

identity handling   ~30 lines  ->  1 line, best-effort tag
duplicated constant  18 entries ->  0

The event lands in the same project as DISCOVERY-TOOL-SCRIPT-17, which is where these failures actually get investigated. Tagged phase:installer_blocked with reason, domain, branch and PowerShell version.

The title carries the reason code only, never the per-machine detail — that is the dispatch.lock mistake, where a username in the message split one bug into 44 issues.

Traded away: joining blocked machines to their org's device rows in SQL. The domain tag identifies the customer and device_id the machine, which answers the same question without needing a key that has to be exactly right. If the SQL join is wanted later, it can be added once identity is resolved properly rather than re-derived in PowerShell.

Mutation-tested both new guards:

$Detail in the title      -> test_issue_title_carries_only_the_reason_code   FAILED
DSN guard removed         -> test_sentry_dsn_can_be_disabled_for_tests       FAILED
as-is                     -> 6 passed, 2 skipped

@vigneshsubbiah16 vigneshsubbiah16 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛡️ Automated Security Review (consensus)

3 findings — 0 high-confidence, 3 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.


🟡 Unredacted download exception forwarded to Sentry

install.ps1:140 → install.ps1:228 (payload at install.ps1:73)

Impact: repo-download-failed ships raw .Exception.Message in extra.detail; proxy URLs, query-string tokens, user:pass@ URLs, and local paths can reach Sentry and anyone with project access.

Fix: Classify failures (tls-error, http-403, etc.) and send a scrubbed, length-capped detail — strip ://…@, query strings, and path segments before upload.

Flagged by: Claude


🟡 AI_DISCOVERY_SENTRY_DSN parsed without validation

install.ps1:48-49, install.ps1:76

Impact: A process-local env override (MDM, scheduled task, setx) can redirect installer telemetry — hostname, domain, branch, serial — to an attacker-controlled host while still using the embedded key material.

Fix: Allow-list the override against a Sentry DSN regex (or ignore non-matching values); empty/unset should remain the only “disabled” path.

Flagged by: Claude


🟡 Hardcoded ingest DSN enables third-party event injection

install.ps1:43-44

Impact: The committed DSN is trivially scrapable from the distributed script; anyone can POST noise into the same project as agent telemetry (DISCOVERY-TOOL-SCRIPT-17), burning quota and diluting installer_blocked signal.

Fix: Acceptable if intentional — add Sentry inbound filters / spike protection, per-key rate limits, and/or a separate installer-only DSN so poisoning cannot mask agent events.

Flagged by: Claude


Previously acknowledged (not re-flagged)

  • Silent catch on reporting failures — deliberate: must not change exit code or delay on an already-broken install (“Fails silently by design”, PR description).
  • Hostname / BIOS serial / domain in Sentry tags — accepted: matches existing agent tagging practice (“No PII beyond existing practice”, PR description).
  • Exact device_id parity with the agent probe chain — accepted tradeoff after Sentry redesign: device_id is a best-effort tag, not a DB primary key; SQL row correlation was explicitly traded away (@anonpran).
  • HTTP Bearer key on explicit http:// Domain — fixed in 61ba347 (HTTPS guard mirrored from branch lookup).
  • Placeholder serial / CIM–WMI probe mismatches / WMIC array join — fixed, then superseded by removal of Get-DeviceId when reporting moved off the lifecycle endpoint.

🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 60729991 · 2026-09-02T06:13Z

The tag claimed to be the agent's device_id while carrying an unvalidated
BIOS value, so a placeholder or an empty string looked like a device identity
rather than a hardware reading. Renaming it removes the claim: bios_serial is
the raw value and is allowed to be junk or absent, which is itself a useful
signal about the machine.

Correlation was never resting on it. The agent tags hostname on every Sentry
event (utils.py:2243, platform.node()) and this event tags $env:COMPUTERNAME,
which is the same value on Windows, so installer and agent events for one
machine already join on hostname whatever the BIOS returns.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7m2NkfcYw536ZbmPJ7kfu
@anonpran

anonpran commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Half agree, and I've fixed the half I agree with — but by renaming the tag rather than reinstating the identity chain.

Agreed: calling it device_id was wrong. That name claims it is the agent's resolved identity, and it was carrying a raw, unvalidated BIOS reading that can be a placeholder or empty. Fixed: it is now bios_serial, which is what it actually is. A placeholder or blank value is now an honest hardware reading rather than a broken identity — and is itself a useful signal.

Disagree that the chain is needed, for three reasons:

1. Correlation was never resting on the serial. The agent tags hostname on every Sentry event (utils.py:2243, platform.node()), and this event tags $env:COMPUTERNAME — the same value on Windows. So installer and agent events for one machine already join on hostname whatever the BIOS returns. Nothing is uncorrelatable.

2. A tag cannot misattribute. On the previous DB design device_id was a primary key, so a placeholder merged forty machines into one row. Sentry events are individual; two events sharing a tag value are still two events, told apart by hostname. The harm that justified the chain does not exist here.

3. The lookup the serial would enable does not apply. Its unique value over hostname is finding the machine in our dashboard — but a blocked machine has no device row at all, because the agent never ran. That is the entire premise of this PR. And once it later scans successfully, the correlation works out anyway: if the agent keyed that row on the serial then the serial was valid and mine matches; if it fell back to hostname, the hostname tag matches.

So the chain would add ~25 lines and a second copy of INVALID_SERIAL_VALUES to improve a tag that is not a key, for machines whose device row does not exist.

relabelled device_id  -> test_raw_serial_is_not_labelled_device_id   FAILED
as-is                 -> 7 passed, 2 skipped

Happy to be overruled if you see a query that needs installer events keyed on the agent's resolved device_id specifically — I could not construct one where hostname does not serve.

@vigneshsubbiah16 vigneshsubbiah16 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛡️ Automated Security Review (consensus)

2 findings — 0 high-confidence, 2 to triage. Reviewers: Claude, Semgrep, Gitleaks, Lead (Cursor unavailable).

🟡 Unsanitized download exception text posted to Sentry

install.ps1:141, install.ps1:72
Impact: repo-download-failed events ship the raw .Exception.Message, which can embed proxy credentials (http://user:pass@...), tokenized URLs, or C:\Users\<name>\... paths into a third-party store visible to all Sentry project members.
Fix: Sanitize extra.detail before send (strip URL userinfo/query, redact user-profile paths, cap length) or send only exception type plus HTTP status.
Flagged by: Claude, Lead

🟡 Hardcoded public Sentry DSN in distributed installer

install.ps1:42
Impact: The ingest key is client-visible by Sentry design, but anyone with the repo can forge installer_blocked events or burn project quota, degrading the signal this PR adds.
Fix: Enable per-key rate limits/spike protection; consider a dedicated installer DSN/key so abuse can be rotated without touching agent reporting.
Flagged by: Claude

Previously acknowledged (not re-flagged)

  • Empty outer catch {} on Send-InstallerFailure — Maintainer: fails silently by design; must not change exit code or add delay on an already-broken install.
  • AI_DISCOVERY_SENTRY_DSN fully overrides the DSN — Maintainer: intentional; empty disables sending and matches how the agent is silenced in tests.
  • bios_serial / hostname tags without agent identity chain — Maintainer: accepted design after Sentry rework; tags are not DB keys, hostname correlates with agent events, and blocked machines have no device row to join anyway.
  • Greptile HTTP Bearer-key / device_id probe findings — Addressed in prior commits (HTTPS guard, probe parity, then Sentry migration); not re-raised.

🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 8335ea34 · 2026-09-02T06:46Z

The old wording referred to "these paths" from the function definition,
pointing at call sites 170 lines below. The question a reader actually has is
why this does not use utils.py's Sentry reporter, and the answer is that the
agent is not downloaded yet at that point.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7m2NkfcYw536ZbmPJ7kfu
@cursor

cursor Bot commented Sep 2, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@vigneshsubbiah16 vigneshsubbiah16 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛡️ Automated Security Review (consensus)

1 finding — 0 high-confidence, 1 to triage. Reviewers: Cursor (unavailable), Claude, Semgrep, Gitleaks.

🟡 TRIAGE — Download exception text may leak credentials to Sentry

install.ps1:141 → install.ps1:74
Impact: $script:LastDownloadError forwards the raw .Exception.Message into extra.detail; PowerShell web/proxy errors often embed full request URIs or echoed auth headers, which would be stored in Sentry off-box.
Fix: Scrub URLs/query strings and cap length before send (e.g. exception type + HTTP status); only include a redacted message fragment.
Flagged by: Claude

Previously acknowledged (not re-flagged)

  • HTTP Bearer key over cleartext http:// domain — Fixed in 61ba347; Sentry path no longer posts to the scan API, and the prior HTTPS guard issue was addressed.
  • Device identity probe chain / placeholder serials / device_id tag — Resolved by removing Get-DeviceId and switching to Sentry; bios_serial is an intentional best-effort hardware tag, with hostname as the correlation key (@anonpran).
  • Empty outer catch on Send-InstallerFailure — Accepted by design: must not change exit code or add delay on an already-broken install (PR description).
  • hostname / bios_serial in Sentry tags — Accepted: matches existing agent telemetry practice (“no PII beyond existing practice”).
  • AI_DISCOVERY_SENTRY_DSN env override — Accepted: matches how the agent is silenced in tests; URI construction forces https:// for the ingest host.
  • Hardcoded Sentry DSN — Accepted: client-side ingest keys are public by design and match the agent’s existing pattern; enable Sentry inbound filters/rate limits if quota abuse is a concern.

🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head bfbb8cc2 · 2026-09-02T07:01Z

install.ps1:181 exits when -ApiKey or -Domain is absent, which is the same
silent-exit shape the other two gates had. An MDM push with a malformed
argument dies earliest and told nobody.

Self-selecting by construction: Send-InstallerFailure returns early without a
resolved domain, so this fires for a missing key and stays quiet for a missing
domain, which has nothing to attribute the event to anyway.

Detail carries booleans, never the key itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@vigneshsubbiah16 vigneshsubbiah16 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛡️ Automated Security Review (consensus)

4 findings — 0 high-confidence, 4 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.

🟡 Unscrubbed exception text sent to Sentry

install.ps1:141 → install.ps1:76
Impact: repo-download-failed forwards $_.Exception.Message verbatim in extra.detail; .NET web errors can embed full URLs, proxy/auth endpoints, or local paths (usernames) into a shared third-party project.
Fix: Scrub/truncate before send (strip user:pass@, token-like substrings) or map to a small set of classified reason strings; keep raw text local-only.
Flagged by: Claude, Lead

🟡 AI_DISCOVERY_SENTRY_DSN can redirect telemetry

install.ps1:43
Impact: Any process that can set the installer's environment can point events (hostname, bios_serial, domain, exception detail) at an arbitrary HTTPS host; the empty outer catch hides failed/malicious delivery.
Fix: Only honor overrides whose host matches an allowlist (e.g. *.ingest.sentry.io / *.sentry.io); treat empty as disabled; fall back to the built-in DSN otherwise.
Flagged by: Claude, Lead

🟡 TLS 1.2 OR'd without disabling legacy protocols

install.ps1:74
Impact: [Net.ServicePointManager]::SecurityProtocol -bor Tls12 leaves SSL 3.0 / TLS 1.0 enabled for the rest of the PowerShell session, slightly widening downgrade surface for later HTTPS in the same process.
Fix: Assign explicitly, e.g. = Tls12 (and Tls13 where supported) instead of OR-ing into the existing bitmask.
Flagged by: Claude, Lead

🟡 Hardcoded public Sentry DSN enables event flooding

install.ps1:42
Impact: The write-only DSN is readable from the shipped installer; anyone can POST forged installer_blocked events and pollute or consume project quota, diluting the signal this PR adds.
Fix: Accept the tradeoff with inbound Sentry filters/rate limits on phase:installer_blocked, or rotate/limit at the project level.
Flagged by: Claude, Lead

Previously acknowledged (not re-flagged)

  • Bearer key over cleartext HTTP — Fixed in 61ba347; reporting moved off /api/v1/ai-tools/report/ to Sentry entirely.
  • Empty outer catch on reporting — Fail-silent by design so a broken install is not delayed or given a different exit code.
  • bios_serial / hostname / domain in Sentry tags — Accepted as matching existing agent telemetry practice; relabelled from device_id because tags are not DB primary keys and hostname is the correlation key.
  • Full device-ID probe chain in PowerShell — Removed after pivot to Sentry; maintainer accepted best-effort bios_serial tag over duplicating agent identity logic.

🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 0abe7a49 · 2026-09-03T05:01Z

…e separable

send_scan_event delegates to send_report_to_backend, so its failures were already
reaching Sentry -- but stamped "phase: send_report", identical to an inventory
upload failure. In the events we have today there is no way to tell whether a
device died announcing itself or died delivering results. Those are opposite
diagnoses: the first never got off the ground, the second scanned fine and could
not deliver.

Pass phase=scan_event:<event> from send_scan_event, and flip the six
report_to_sentry call sites from {**ctx, "phase": "send_report"} to
{"phase": "send_report", **ctx} so a caller-supplied phase wins instead of being
clobbered by the spread order. Default is unchanged for direct callers.

Makes "how many devices never started" answerable as a Sentry query.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@anonpran
anonpran force-pushed the nanda/installer-failure-reporting branch from 3606f61 to d00244c Compare September 3, 2026 05:11

@vigneshsubbiah16 vigneshsubbiah16 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛡️ Automated Security Review (consensus)

4 findings — 1 high-confidence, 3 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.

Repo-download exception text forwarded verbatim to Sentry

install.ps1:141, install.ps1:76, install.ps1:232
Impact: Raw .NET/PowerShell exception messages in extra.detail can include proxy hosts, local temp paths (often username-bearing), and download URLs—if those URLs ever carry tokens or pre-signed credentials, they leave the machine unredacted.
Fix: Scrub sensitive patterns (URLs, token/key/password query params) and cap detail length (e.g. 1024 chars) before Send-InstallerFailure.
Flagged by: Claude, Cursor

AI_DISCOVERY_SENTRY_DSN can redirect reports to an arbitrary host

install.ps1:42
Impact: Any principal that can set a user or machine environment variable can point installer telemetry (hostname, BIOS serial, tenant domain, branch, failure detail) at an attacker-controlled ingest endpoint—low-noise exfiltration without elevation.
Fix: Only honor overrides that match an expected Sentry DSN shape/host allow-list; treat anything else as disabled.
Flagged by: Claude

Embedded Sentry DSN enables forged installer_blocked events

install.ps1:41
Impact: The public client key is write-only by design, but anyone with the script can inject unlimited forged events into the same project, polluting the signal this PR adds and consuming quota.
Fix: Enable per-key rate limiting and inbound filters in Sentry; accept as trade-off only if it mirrors the agent's existing embedded DSN policy.
Flagged by: Claude

TLS protocol bitmask OR leaves legacy protocols enabled

install.ps1:72
Impact: -bor Tls12 adds TLS 1.2 without disabling older defaults (Ssl3, Tls 1.0) for the remainder of the process; a network attacker could theoretically downgrade, though Sentry ingest itself requires TLS 1.2+.
Fix: Assign the allowed set explicitly (Tls12 / Tls13 inside the existing try) instead of OR-ing onto the process default.
Flagged by: Claude

Previously acknowledged (not re-flagged)

  • Device identity probe mismatches / placeholder serial as device_id — Maintainer removed Get-DeviceId and the duplicated INVALID_SERIAL_VALUES chain; failures now go to Sentry as tags. bios_serial is an honest raw hardware reading (not resolved device_id); correlation uses hostname, and tags cannot corrupt DB rows.
  • HTTP Bearer key sent over cleartext http:// domain — Superseded: reporting now posts directly to Sentry over HTTPS (https://$sentryHost/.../store/), not the tenant /api/v1/ai-tools/report/ endpoint.
  • Empty outer catch on Send-InstallerFailure — Accepted by design: must not change exit code or add delay on an already-broken install (10s cap, fail silently).
  • Hostname / BIOS serial in telemetry — Accepted: matches existing agent practice (“No PII beyond existing practice” in PR description).

🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head d00244c7 · 2026-09-03T05:13Z

Under MDM there is no console attached, so every message install.ps1 prints is
discarded when the process exits. A customer whose rollout failed has nothing to
send us, and Sentry only helps if their network lets us reach it -- Xome has 143
devices and zero reports in 14 hours.

Reuses setup-scheduled-scan.ps1's convention rather than inventing one:
%LOCALAPPDATA%\Unbound\Logs, same timestamped line format, install.log next to
scheduled.log. One directory for support to ask about, not two. Write-Log never
raises, so logging cannot become the failure.

Write-ErrorMessage now echoes the resolved path, so the reply to a customer is a
copy-paste rather than an explanation of the convention.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@vigneshsubbiah16 vigneshsubbiah16 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛡️ Automated Security Review (consensus)

2 findings — 0 high-confidence, 2 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.

🟡 Unbounded exception detail sent to Sentry and written to disk

install.ps1:158, install.ps1:251 · Claude

Download failures stash the full Exception.Message in $script:LastDownloadError and forward it unchanged to Sentry (extra.detail) and install.log via Write-Warning/Write-ErrorMessage, with no length cap or scrubbing.

Impact: Proxy/HTTP/TLS errors can embed request URIs or environment-specific paths; if any download URL ever includes the API key, that value leaves the machine to a third party and is persisted locally.

Fix: Mirror the Python side (response_body[:1024], curl_stderr[:1024]): redact $_key/$ApiKey from $Detail before Send-InstallerFailure and Write-Log, then truncate to 1024 characters.


🟡 Multi-line exception text can forge log lines

install.ps1:38 · Claude

Write-Log formats "[{timestamp}] {msg}" with $msg verbatim; exception strings containing CR/LF produce additional lines that look like independent timestamped entries.

Impact: install.log is the support artifact customers are asked to send; injected line breaks weaken it as trustworthy evidence.

Fix: Flatten before writing: $msg = ($msg -replace '[\r\n]+', ' ').


Previously acknowledged (not re-flagged)

  • HTTP Bearer key over cleartext http:// domain — Fixed in 61ba347; reporting later moved off the backend endpoint to Sentry entirely (@anonpran).
  • Device identity mismatch (device_id, placeholder serials, CIM/WMIC probe chain) — Reworked to best-effort bios_serial Sentry tag; correlation relies on hostname, which the agent already tags (@anonpran: tag cannot misattribute; blocked machines have no device row).
  • Silent catch { } on Send-InstallerFailure — Accepted by design: must not change exit code or add delay on an already-broken install (PR design notes).
  • AI_DISCOVERY_SENTRY_DSN environment override — Deliberate; mirrors agent test silencing (PR design notes).
  • hostname / bios_serial sent to Sentry — Accepted; no PII beyond existing agent practice (PR design notes).

🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 3b34cb0e · 2026-09-03T05:21Z

Both review findings share a cause: Exception.Message is third-party, unbounded
and forwarded verbatim. One Format-Detail helper on both sinks.

Flatten CR/LF: Write-Log formats "[ts] $msg", so an embedded newline produces
extra lines that read as separate entries. install.log is the artifact we ask
customers to send, so it has to stay one-entry-per-line.

Redact the key: no current download URL carries it (the archive comes from
github.com and the branch check sends a Bearer header), but the log is written
to disk and the Sentry payload leaves the machine, so this is cheap insurance
rather than a fix for a known leak.

Cap at 1024, matching response_body[:1024] and curl_stderr[:1024] on the Python
side.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@vigneshsubbiah16 vigneshsubbiah16 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛡️ Automated Security Review (consensus)

3 findings — 0 high-confidence, 3 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.

🟡 TRIAGE

1. AI_DISCOVERY_SENTRY_DSN can redirect installer telemetry to an arbitrary host
install.ps1:75

  • Impact: A process that can set $env:AI_DISCOVERY_SENTRY_DSN (e.g. per-user HKCU\Environment) can point failure reports at a server they control, exfiltrating hostname, bios_serial, customer domain, branch, and exception detail; the fire-and-forget call hides the misdirection.
  • Fix: Honor the override only when the parsed host matches an allowlist (e.g. *.ingest.*.sentry.io), or restrict overrides to an explicit test-only switch and keep production on the embedded DSN.
  • Flagged by: Claude

2. Exception detail redaction is literal-only; credentials in third-party text may still leak
install.ps1:36

  • Impact: Format-Detail scrubs only an exact match of $_key; proxy URLs (user:pass@), URL-encoded keys, and other embedded secrets in download/proxy errors can reach Sentry extra.detail and %LOCALAPPDATA%\Unbound\Logs\install.log (a support artifact).
  • Fix: Add pattern-based scrubbing (URI credentials, case-insensitive and URL-encoded key forms) on top of the literal replace; consider reusing the Python-side redaction rules if they exist.
  • Flagged by: Claude

3. TLS protocol set uses -bor, leaving weaker protocols enabled
install.ps1:98

  • Impact: [Net.ServicePointManager]::SecurityProtocol -bor Tls12 adds TLS 1.2 without disabling SSL 3.0 / TLS 1.0 already enabled in the process, mutating TLS behavior for all subsequent HTTPS in the installer (archive download included).
  • Fix: Assign explicitly, e.g. [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13.
  • Flagged by: Claude

Previously acknowledged (not re-flagged)

  • HTTP Domain sent Bearer API key over cleartext — Fixed in 61ba347; reporting later moved off the backend endpoint entirely (@anonpran).
  • Placeholder / mismatched device_id and multi-probe identity chain — Fixed across several commits, then removed with the Sentry rework; device_id is now bios_serial as an honest best-effort tag, not a primary key (@anonpran).
  • domain, hostname, and raw bios_serial in Sentry events — Accepted as within existing agent telemetry practice; hostname is the correlation key (@anonpran).
  • Empty catch on Send-InstallerFailure hides reporting failures — Accepted by design: prerequisite exits must not change exit code or add delay beyond the 10s cap (PR description).

🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 8f66bc1e · 2026-09-03T05:42Z

This branch has not been deployed

No deployments
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.

2 participants